Skip to content

Instantly share code, notes, and snippets.

@TonytheMacaroni
Last active June 14, 2023 17:34
Show Gist options
  • Save TonytheMacaroni/f04aaf8c5629db1d8ca1c9a7fa9593fe to your computer and use it in GitHub Desktop.
Save TonytheMacaroni/f04aaf8c5629db1d8ca1c9a7fa9593fe to your computer and use it in GitHub Desktop.

Instructions

  1. Ensure you have Python 3 installed. If not, follow the instructions here for installation.

  2. Ensure an up-to-date version of the ruamel.yaml library is installed, as documented here. Optionally, instead install using the supplied requirements.txt and using the following command:

pip install -r requirements.txt
  1. Backup any files contained within the folder you're executing the script, and execute the script on a copy, not the source configuration files. Test out the script on a small batch of spell files first to ensure proper formatting. Open a command line in the folder containing the spells you wish to modify, and run the script with the following arguments.

  2. Download and run the script in the directory you wish to modify, as specified in step 3. The script has the following arguments:

    • -i, --indent - used to specify the indent of the output file, and defaults to 4. For example --indent 4 results in the script's output having an indentation of 4 spaces.

    • -w, --max_width - used to specify the maximum line width, and defaults to 4096. The library used for parsing YAML files wraps lines according to a maximum width.

    • -b, --block_seq_indent - used to specify the indentation used for block sequences, and defaults to the same value as the indentation. For example, with --indent 4 the output is as following:

      key:
          key:
              - value
              - value

      But if --indent 4 --block_seq_indent 2 is specified, the output is as following:

      key:
          key:
            - value
            - value
    • -n, --include_interactions - flag used to specify that spells involved in interactions should be modified as well. As the bug fix only pertains towards the radius when hitting entities and not interactions between particle projectiles, special consideration may need to be made for spells using interactions. As such, by default, they are ignored by the script, and instead output a message when ignored. This flag overrides this behavior.

    • file_globs - list of globs used to determine the files to be modified. The usage of ** is recursive in this case. For example, **/spell*.yml matches any file starting with spell and ending in .yml in the current folder, as well as all subdirectories, recursively.

Example

The following is an example usage of the script:

python radius.py --indent 4 --block_seq_indent 2 spell*.yml

This command will modify all files starting with spell and ending with .yml in the current directory of the command line, with the modified output having an indent of 4 spaces and a block sequence indent of 2 spaces. If the following configuration was present:

# spells-test.yml

test-projectile:
    spell-class: ".instant.ParticleProjectileSpell"
    tags:
        - projectile
        - ranged
    can-cast-by-command: false
    beneficial: false
    cooldown: 10
    spell: test-projectile-damage
    stop-on-hit-entity: false
    stop-on-hit-ground: false
    vertical-hit-radius: 1
    hit-players: true
    hit-ground: false
    hit-radius: 2
    effects:
        effect1:
            position: special
            effect: effectlib
            effectlib:
                class: VortexEffect
                color: d10000
                particle: SPELL_INSTANT
                iterations: 1
                particles: 25
                helixes: 5
                yOffset: 1
                period: 1
                radius: 8
                speed: 1

test-projectile-damage:
    spell-class: ".targeted.PainSpell"
    tags:
        - ranged
        - physical
    beneficial: false
    damage-type: PROJECTILE
    damage: 10

After running the script with the above arguments, the file would be:

# spells-test.yml

test-projectile:
    spell-class: ".instant.ParticleProjectileSpell"
    tags:
      - projectile
      - ranged
    can-cast-by-command: false
    beneficial: false
    cooldown: 10
    spell: test-projectile-damage
    stop-on-hit-entity: false
    stop-on-hit-ground: false
    vertical-hit-radius: 0.5
    hit-players: true
    hit-ground: false
    hit-radius: 1
    effects:
        effect1:
            position: special
            effect: effectlib
            effectlib:
                class: VortexEffect
                color: d10000
                particle: SPELL_INSTANT
                iterations: 1
                particles: 25
                helixes: 5
                yOffset: 1
                period: 1
                radius: 8
                speed: 1

test-projectile-damage:
    spell-class: ".targeted.PainSpell"
    tags:
      - ranged
      - physical
    beneficial: false
    damage-type: PROJECTILE
    damage: 10

Note that the hit-radius and vertical-hit-radius of the spell halved from 2 to 1 and from 1 to 0.5 respectively, and with --block_seq_indent 2, the tags block sequences were indented with 2 spaces. Additionally, the script would have the following output:

  'test-projectile', 'hit-radius': '2' -> '1'
  'test-projectile', 'vertical-hit-radius': '1' -> '0.5'
Modified spell file spells-test.yml.

With the following format for each spell:

  <spell name>, <option name>: '<previous value>' -> '<new value>'
Modified spell file <file name>.

The message when ignoring a spell with interactions is as follows:

  Ignoring spell with interactions '<spell name>' in file '<file name>'.

Notes

  • As mentioned earlier, you should back up any folders or files this script will be ran on. Incorrect usage of the script may lead to incorrect formattting, and backups ensure that any problems can be rolled back without issue.

  • Both SnakeYAML (which is used for parsing MagicSpells configurations in the plugin) and this script ignore duplicate keys. However, when the scripts outputs, these duplicates are discarded. Caution should be exercised as this script (as per YAML spec) considers the first duplicate key, while SnakeYAML considers the last duplicate key. As such, this configuration:

    test:
        spell-class: ".instant.DummySpell"
        str-cast-self: "Test 1."
    
    test:
        spell-class: ".instant.DummySpell"
        str-cast-self: "Test 2."

    Would have a message of Test 2. when casted, but this script would convert this configuration with duplicate keys to:

    test:
        spell-class: ".instant.DummySpell"
        str-cast-self: "Test 1."

    Run a YAML linter to scan for duplicates like this before running the script, as these duplicates are discarded silently. Optionally, you can instead change line 24 in radius.py to yaml.allow_duplicate_keys = False, so that files with duplicates are not loaded.

from ruamel.yaml import YAML, CommentedMap, CommentedSeq
import itertools
import traceback
import argparse
import glob
def main():
parser = argparse.ArgumentParser(
prog="PPJ Radius Adjuster",
description="Doubles the hit-radius and vertical-hit-radius of ParticleProjectileSpell and ProjectileModifySpell configurations."
)
parser.add_argument("-i", "--indent", type=int, default=4, help="indent of output")
parser.add_argument("-w", "--max_width", type=int, default=4096, help="maximum line width of output")
parser.add_argument("-b", "--block_seq_indent", type=int, default=None, help="indent of block sequences in output, defaults to indent value")
parser.add_argument("-n", "--include_interactions", action='store_true', help="if included, projectiles with interactions will also be modified")
parser.add_argument("file_globs", nargs='+', help="globs of files to modify")
args = parser.parse_args()
yaml = YAML()
yaml.allow_duplicate_keys = True
yaml.preserve_quotes = True
if args.block_seq_indent is None:
yaml.block_seq_indent = args.indent
else:
yaml.block_seq_indent = args.block_seq_indent
yaml.width = args.max_width
yaml.indent = args.indent
globs = [glob.glob(item, recursive=True) for item in args.file_globs]
paths = set(itertools.chain.from_iterable(globs))
for path in paths:
fix_radius(yaml, path, args.include_interactions)
def fix_radius(yaml: YAML, path: str, include_interactions: bool):
try:
with open(path, mode='r') as yaml_file:
document = yaml.load(yaml_file)
except Exception as e:
print(f"Could not load file at {path}.")
traceback.print_exception(e)
return
if not isinstance(document, CommentedMap):
return
spells_to_modify: dict[str, CommentedMap] = {}
spells_with_interactions: set[str] = set()
for internal_name, options in document.items():
if not isinstance(options, CommentedMap) or "spell-class" not in options:
continue
spell_class = options["spell-class"]
if not isinstance(spell_class, str):
continue
if spell_class.startswith("."):
spell_class = "com.nisovin.magicspells.spells" + spell_class
if spell_class != "com.nisovin.magicspells.spells.instant.ParticleProjectileSpell" and \
spell_class != "com.nisovin.magicspells.spells.targeted.ProjectileModifySpell":
continue
if spell_class == "com.nisovin.magicspells.spells.instant.ParticleProjectileSpell" and "interactions" in options:
interactions = options["interactions"]
if isinstance(interactions, CommentedSeq):
spells_with_interactions.update(get_interacted_spells(internal_name, interactions))
spells_to_modify[internal_name] = options
dirty = False
for internal_name, options in spells_to_modify.items():
if not include_interactions and internal_name in spells_with_interactions:
print(f" Ignoring spell with interactions '{internal_name}' in file '{path}'.")
continue
dirty = modify_radius(internal_name, options, "hit-radius") or dirty
dirty = modify_radius(internal_name, options, "vertical-hit-radius") or dirty
if dirty:
try:
with open(path, mode='w') as yaml_file:
yaml.dump(document, yaml_file)
print(f"Modified spell file {path}.")
except Exception as e:
print(f"Could not dump file to {path}.")
print(e)
def modify_radius(internal_name: str, options: CommentedMap, option_name: str) -> bool:
if option_name not in options:
return False
radius = options[option_name]
if not isinstance(radius, (int, float)):
return False
new_radius = radius / 2
if isinstance(new_radius, float) and new_radius.is_integer():
new_radius = int(new_radius)
if radius == new_radius:
return False
options[option_name] = new_radius
print(f" '{internal_name}', '{option_name}': '{radius}' -> '{new_radius}'")
return True
def get_interacted_spells(internal_name: str, interactions: CommentedSeq):
interacted = False
for interaction in interactions:
if not isinstance(interaction, str):
continue
interacted = True
yield interaction.split(" ")[0]
if interacted:
yield internal_name
if __name__ == '__main__':
main()
ruamel.yaml~=0.17.31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment