forgottenserver icon indicating copy to clipboard operation
forgottenserver copied to clipboard

Move spells to scripts folder

Open ranisalt opened this issue 1 year ago • 9 comments

Pull Request Prelude

Changes Proposed

Moves player spells to data/scripts in order to reduce XML-coding. Monster spells, rune spells and house spells stay in XML for now :laughing:

ranisalt avatar Jun 15 '24 12:06 ranisalt

nice, can we keep tab for indentation just to keep aligned with the other files? 😄 and also I see some spells were deleted accidentally? for example magic shield and cancel magic shield

ghost avatar Jun 15 '24 18:06 ghost

nice, can we keep tab for indentation just to keep aligned with the other files?

It was formatted with luaformatter, I don't know why it used spaces but maybe we're missing a config file?

some spells were deleted accidentally? for example magic shield and cancel magic shield

oops

ranisalt avatar Jun 15 '24 21:06 ranisalt

nice, can we keep tab for indentation just to keep aligned with the other files?

All files use spaces rather than tabs, are you sure about that? :thinking:

ranisalt avatar Jun 15 '24 23:06 ranisalt

We can make loading not fail if XML is not present and drop the file altogether

ranisalt avatar Jun 16 '24 00:06 ranisalt

We can make loading not fail if XML is not present and drop the file altogether

I we wont drop support for xml i preffer to keep this file with some samples (like revscript examples)

ArturKnopik avatar Jun 16 '24 18:06 ArturKnopik

I we wont drop support for xml i preffer to keep this file with some samples (like revscript examples)

The goal is to drop XML

ranisalt avatar Jun 16 '24 22:06 ranisalt

Droping XML support will break backward compatibility. As far as i know TFS want to be backward compat. PS: There is no automated script to convert all xml spells to revscript format.

ArturKnopik avatar Jun 17 '24 06:06 ArturKnopik

Droping XML support will break backward compatibility. As far as i know TFS want to be backward compat.

It won't, we can supply a XML parser written in Lua that can be disabled at will. I will do that in a separate PR while disabling it in C++

There is no automated script to convert all xml spells to revscript format.

Indeed :laughing: I used this script written in Python to mass-convert them:

import xml.etree.ElementTree as ET
from os import makedirs, path, unlink

tree = ET.parse("spells.xml")
root = tree.getroot()
assert root.tag == "spells"

for child in root:
    if "script" not in child.attrib:
        continue

    lines = []

    for attrib in child.attrib:
        if attrib in ["script"]:
            continue

        match attrib.lower():
            case "spellid":
                lines.append(f"spell:id({child.attrib[attrib]})")
            case "id" if child.tag == "rune":
                lines.append(f"spell:runeId({child.attrib[attrib]})")
            case "allowfaruse" if child.tag == "rune":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:allowFarUse(true)")
            case "charges" if child.tag == "rune":
                lines.append(f"spell:charges({child.attrib[attrib]})")
            case "blocktype" if child.tag == "rune":
                match child.attrib[attrib]:
                    case "all":
                        lines.append(f"spell:isBlocking(true, true)")
                    case "solid":
                        lines.append(f"spell:isBlocking(true, false)")
                    case "creature":
                        lines.append(f"spell:isBlocking(false, true)")
            case "group":
                lines.append(f'spell:group("{child.attrib[attrib]}")')
            case "secondarygroup":
                lines = [line if not line.startswith("spell:group") else f'spell:group("{child.attrib["group"]}", "{child.attrib[attrib]}")' for line in lines]
            case "name":
                lines.append(f'spell:name("{child.attrib[attrib]}")')
            case "words":
                lines.append(f'spell:words("{child.attrib[attrib]}")')
            case "level" if child.tag == "spell":
                lines.append(f"spell:level({child.attrib[attrib]})")
            case "magiclevel" if child.tag == "spell":
                lines.append(f"spell:magicLevel({child.attrib[attrib]})")
            case "level" if child.tag == "rune":
                lines.append(f"spell:runeLevel({child.attrib[attrib]})")
            case "magiclevel" if child.tag == "rune":
                lines.append(f"spell:runeMagicLevel({child.attrib[attrib]})")
            case "mana":
                lines.append(f"spell:mana({child.attrib[attrib]})")
            case "soul":
                lines.append(f"spell:soul({child.attrib[attrib]})")
            case "premium":
                if child.attrib[attrib] == "0":
                    lines.append(f"spell:isPremium(true)")
            case "direction":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:needDirection(true)")
            case "aggressive":
                if child.attrib[attrib] == "0":
                    lines.append(f"spell:isAggressive(false)")
            case "playernameparam":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:hasPlayerNameParam(true)")
            case "params":
                if child.attrib[attrib] == "1":
                    lines.append(f'spell:hasParams(true)')
            case "selftarget":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:isSelfTarget(true)")
            case "pzlock" if child.tag == "rune":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:isPzLocked(true)")
            case "range":
                lines.append(f"spell:range({child.attrib[attrib]})")
            case "castertargetordirection":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:needCasterTargetOrDirection(true)")
            case "blockwalls":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:blockWalls(true)")
            case "needtarget":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:needTarget(true)")
            case "needweapon":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:needWeapon(true)")
            case "cooldown":
                lines.append(f"spell:cooldown({child.attrib[attrib]})")
            case "groupcooldown":
                lines.append(f"spell:groupCooldown({child.attrib[attrib]})")
            case "secondarygroupcooldown":
                lines = [line if not line.startswith("spell:groupCooldown") else line[:-1] + f", {child.attrib[attrib]})" for line in lines]
            case "needlearn":
                if child.attrib[attrib] == "1":
                    lines.append(f"spell:needLearn(true)")
            case _:
                print(f"Unknown attribute: {attrib}")

    with open(f"scripts/{child.attrib['script']}", "r") as f:
        spell = f.read().split("\n")

    voc_list = []
    for voc in child:
        assert voc.tag == "vocation"
        voc_list.append((voc.attrib['name'].lower(), voc.attrib.get('showInDescription', '1') == '1'))

    if voc_list:
        lines.append(f'spell:vocation({", ".join(f'"{voc};true"' if show else f'"{voc}"' for voc, show in voc_list)})')

    for idx, line in enumerate(spell):
        if line.startswith("function onCastSpell"):
            break
    else:
        print(f"Spell {child.attrib['name']} does not have onCastSpell function")
        continue

    print(f"Converting spell {child.attrib['name']}")
    makedirs(f"../scripts/spells/{path.dirname(child.attrib['script'])}", exist_ok=True)

    with open(f"../scripts/spells/{child.attrib['script']}", "w") as f:
        f.write(
            "\n".join(
                [
                    *spell[:idx],
                    f"local spell = Spell(SPELL_{child.tag.upper()})\n",
                    *spell[idx:],
                    *lines,
                    "spell:register()",
                ]
            )
        )

    unlink(f"scripts/{child.attrib['script']}")

ranisalt avatar Jun 17 '24 07:06 ranisalt

will be nice to add this script to repo for speedup migration from older version to latest

ArturKnopik avatar Jun 17 '24 10:06 ArturKnopik

There is an issue with this system where reloading scripts while a player is using spells causes the server to crash. /reload scripts

VC tell me the issue from here

spellBlock.spell->castSpell(this, attackedCreature);

`void Monster::doAttacking(uint32_t interval) { if (!attackedCreature || (isSummon() && attackedCreature == this)) { return; }

bool lookUpdated = false;
bool resetTicks = interval != 0;
attackTicks += interval;

const Position& myPos = getPosition();
const Position& targetPos = attackedCreature->getPosition();

for (const spellBlock_t& spellBlock : mType->info.attackSpells) {
	bool inRange = false;

	if (attackedCreature == nullptr) {
		break;
	}

	if (canUseSpell(myPos, targetPos, spellBlock, interval, inRange, resetTicks)) {
		if (spellBlock.chance >= static_cast<uint32_t>(uniform_random(1, 100))) {
			if (!lookUpdated) {
				updateLookDirection();
				lookUpdated = true;
			}

			minCombatValue = spellBlock.minCombatValue;
			maxCombatValue = spellBlock.maxCombatValue;
			spellBlock.spell->castSpell(this, attackedCreature);

			if (spellBlock.isMelee) {
				lastMeleeAttack = OTSYS_TIME();
			}
		}
	}

	if (!inRange && spellBlock.isMelee) {
		//melee swing out of reach
		lastMeleeAttack = 0;
	}
}

// ensure ranged creatures turn to player
if (!lookUpdated && lastMeleeAttack == 0) {
	updateLookDirection();
}

if (resetTicks) {
	attackTicks = 0;
}

}`

ElMan1999 avatar Jan 19 '25 13:01 ElMan1999