Files
Bubberstation/code/modules/unit_tests/strange_reagent.dm
Joshua Kidder 7a3ad79506 All camelCase (Brute|Burn|Fire|Tox|Oxy|Organ|Stamina)(Loss) procs now use snake_case. UNDERSCORES RULE! (#94111)
## About The Pull Request
It's just a partial cleanup of
anti-[STYLE](https://github.com/tgstation/tgstation/blob/master/.github/guides/STYLE.md)
code from /tg/'s ancient history. I compiled & tested with my helpful
assistant and damage is still working.

<img width="1920" height="1040" alt="image"
src="https://github.com/user-attachments/assets/26dabc17-088f-4008-b299-3ff4c27142c3"
/>


I'll upload the .cs script I used to do it shortly.

## Why It's Good For The Game
Just minor code cleanup.

Script used is located at https://metek.tech/camelTo-Snake.7z

EDIT 11/23/25: Updated the script to use multithreading and sequential
scan so it works a hell of a lot faster
```
/*
//
Copyright 2025 Joshua 'Joan Metekillot' Kidder

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
//
*/
using System.Text.RegularExpressions;
class Program
{
    static async Task Main(string[] args)
    {
        var readFile = new FileStreamOptions
        {
            Access = FileAccess.Read,
            Share = FileShare.ReadWrite,
            Options = FileOptions.Asynchronous | FileOptions.SequentialScan
        };
        FileStreamOptions writeFile = new FileStreamOptions
        {
            Share = FileShare.ReadWrite,
            Access = FileAccess.ReadWrite,
            Mode = FileMode.Truncate,
            Options = FileOptions.Asynchronous
        };
        RegexOptions regexOptions = RegexOptions.Multiline | RegexOptions.Compiled;
        Dictionary<string, int> changedProcs = new();
        string regexPattern = @"(?<=\P{L})([a-z]+)([A-Z]{1,2}[a-z]+)*(Brute|Burn|Fire|Tox|Oxy|Organ|Stamina)(Loss)([A-Z]{1,2}[a-z]+)*";
        Regex camelCaseProcRegex = new(regexPattern, regexOptions);

        string snakeify(Match matchingRegex)
        {
            var vals =
            matchingRegex.Groups.Cast<Group>().SelectMany(_ => _.Captures).Select(_ => _.Value).ToArray();
            var newVal = string.Join("_", vals.Skip(1).ToArray()).ToLower();
            string logString = $"{vals[0]} => {newVal}";
            if (changedProcs.TryGetValue(logString, out int value))
            {
                changedProcs[logString] = value + 1;
            }
            else
            {
                changedProcs.Add(logString, 1);
            }
            return newVal;
        }
        var dmFiles = Directory.EnumerateFiles(".", "*.dm", SearchOption.AllDirectories).ToAsyncEnumerable<string>();

        // uses default ParallelOptions
        // https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.paralleloptions?view=net-10.0#main
        await Parallel.ForEachAsync(dmFiles, async (filePath, UnusedCancellationToken) =>
        {
            var reader = new StreamReader(filePath, readFile);
            string oldContent = await reader.ReadToEndAsync();
            string newContent = camelCaseProcRegex.Replace(oldContent, new MatchEvaluator((Func<Match, string>)snakeify));
            if (oldContent != newContent)
            {
                var writer = new StreamWriter(filePath, writeFile);
                await writer.WriteAsync(newContent);
                await writer.DisposeAsync();
            }
            reader.Dispose();
        });
        var logToList = changedProcs.Cast<KeyValuePair<string, int>>().ToList();
        foreach (var pair in logToList)
        {
            Console.WriteLine($"{pair.Key}: {pair.Value} locations");
        }
    }
}

```

## Changelog
🆑 Bisar
code: All (Brute|Burn|Fire|Tox|Oxy|Organ|Stamina)(Loss) procs now use
snake_case, in-line with the STYLE guide. Underscores rule!
/🆑
2025-11-27 15:50:23 -05:00

159 lines
6.3 KiB
Plaintext

/// Checks to ensure strange reagent works as expected
/datum/unit_test/strange_reagent
priority = TEST_LONGER
var/datum/reagent/medicine/strange_reagent/instant/strange_reagent
var/target_max_health
var/amount_needed_to_full_heal
var/amount_needed_to_revive
/datum/unit_test/strange_reagent/Run()
strange_reagent = new
var/list/types_to_check = typecacheof(list(
/mob/living/carbon/human/consistent,
/mob/living/simple_animal,
/mob/living/basic,
))
types_to_check -= typesof(/mob/living/simple_animal/hostile/megafauna) // no
types_to_check -= typesof(/mob/living/basic/mouse) // qdel themselves on death; why dont they use DEL_ON_DEATH you might ask. I-unno
types_to_check -= typesof(/mob/living/basic/slime) // if they roll the 50% chance to spawn as an adult, they can just at random split and qdel themselves
for(var/mob/living/type as anything in types_to_check)
var/mob/living/target = allocate_new_target(type)
var/is_basic = istype(target, /mob/living/basic)
var/is_simple = istype(target, /mob/living/simple_animal)
// check some basic stuff
if(HAS_TRAIT(target, TRAIT_GODMODE))
continue
if(!(target.mob_biotypes & MOB_ORGANIC))
continue
if(is_simple)
var/mob/living/simple_animal/simple_animal = target
if(simple_animal.del_on_death)
continue
simple_animal.loot?.Cut()
if(is_basic)
var/mob/living/basic/basic = target
if(basic.basic_mob_flags & DEL_ON_DEATH)
continue
test_damage_but_no_death(type)
test_death_no_damage(type)
test_death_with_damage(type)
test_death_with_damage_but_not_enough_reagent(type)
if(!is_basic && !is_simple) // simple/basic mobs cannot have negative health
test_death_with_full_heal(type)
test_death_from_damage(type)
test_death_from_too_much_damage(type)
// cleanup our vars
QDEL_NULL(strange_reagent)
allocate_new_target(null)
/datum/unit_test/strange_reagent/proc/allocate_new_target(type)
// cache the last one created so that we don't create N instances of the exact same mob
var/static/mob/living/pre_allocated
if(!type)
pre_allocated = null
return
if(pre_allocated?.type == type)
pre_allocated.revive(ALL) // for some reason HEAL_ADMIN doesn't work?
return pre_allocated
pre_allocated = allocate(type)
target_max_health = pre_allocated.getMaxHealth()
update_amounts(pre_allocated)
return pre_allocated
/datum/unit_test/strange_reagent/proc/update_amounts(mob/living/target)
amount_needed_to_full_heal = strange_reagent.calculate_amount_needed_to_full_heal(target)
amount_needed_to_revive = strange_reagent.calculate_amount_needed_to_revive(target)
/datum/unit_test/strange_reagent/proc/damage_target_to_percentage(mob/living/target, percent)
var/damage = target_max_health * percent * 0.5
target.set_brute_loss(damage, updating_health=FALSE) // no point running health update logic here
target.set_fire_loss(damage, updating_health=TRUE) // since we do it here
update_amounts(target)
if(percent >= 1)
target.death()
return TRUE
/datum/unit_test/strange_reagent/proc/get_target_organic_health_manual(mob/living/target)
return target.getMaxHealth() - (target.get_brute_loss() + target.get_fire_loss())
/datum/unit_test/strange_reagent/proc/test_damage_but_no_death(target_type)
var/mob/living/target = allocate_new_target(target_type)
if(!damage_target_to_percentage(target, 0.8))
return
var/health = get_target_organic_health_manual(target)
strange_reagent.expose_mob(target, INGEST, 20)
TEST_ASSERT_EQUAL(health, get_target_organic_health_manual(target), "Strange Reagent healed a target type [target.type] that was not dead.")
/datum/unit_test/strange_reagent/proc/test_death_no_damage(target_type)
var/mob/living/target = allocate_new_target(target_type)
target.death()
if(QDELETED(target))
return
update_amounts(target)
strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive)
TEST_ASSERT_NOTEQUAL(target.stat, DEAD, "Strange Reagent did not revive a dead target type [target.type].")
/datum/unit_test/strange_reagent/proc/test_death_with_damage(target_type)
var/mob/living/target = allocate_new_target(target_type)
if(!damage_target_to_percentage(target, 0.8))
return
target.death()
if(QDELETED(target))
return
update_amounts(target)
strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive)
TEST_ASSERT_NOTEQUAL(target.stat, DEAD, "Strange Reagent did not revive a dead target type [target.type].")
/datum/unit_test/strange_reagent/proc/test_death_with_damage_but_not_enough_reagent(target_type)
var/mob/living/target = allocate_new_target(target_type)
if(!damage_target_to_percentage(target, 1.2))
return
update_amounts(target)
strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive - 1)
TEST_ASSERT_EQUAL(target.stat, DEAD, "Strange Reagent revived a dead target type [target.type] without enough reagent.")
/datum/unit_test/strange_reagent/proc/test_death_with_full_heal(target_type)
var/mob/living/target = allocate_new_target(target_type)
if(!damage_target_to_percentage(target, 0.8))
return
target.death()
if(QDELETED(target))
return
update_amounts(target)
strange_reagent.expose_mob(target, INGEST, amount_needed_to_full_heal)
TEST_ASSERT_EQUAL(target_max_health, get_target_organic_health_manual(target), "Strange Reagent did not fully heal a dead target type [target.type] with the expected amount.")
/datum/unit_test/strange_reagent/proc/test_death_from_damage(target_type)
var/mob/living/target = allocate_new_target(target_type)
if(!damage_target_to_percentage(target, strange_reagent.max_revive_damage_ratio * 0.9)) // 10% under the damage cap
return
if(QDELETED(target))
return
update_amounts(target)
strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive)
TEST_ASSERT_NOTEQUAL(target.stat, DEAD, "Strange Reagent did not revive a target type [target.type] who died from damage.")
/datum/unit_test/strange_reagent/proc/test_death_from_too_much_damage(target_type)
var/mob/living/target = allocate_new_target(target_type)
if(!damage_target_to_percentage(target, strange_reagent.max_revive_damage_ratio * 1.1)) // 10% over the damage cap
return
if(QDELETED(target))
return
update_amounts(target)
strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive)
TEST_ASSERT_EQUAL(target.stat, DEAD, "Strange Reagent revived a target type [target.type] with more than double their max health in damage.")