Move spells to scripts folder
Pull Request Prelude
- [x] I have followed proper The Forgotten Server code styling.
- [x] I have read and understood the contribution guidelines before making this PR.
- [x] I am aware that this PR may be closed if the above-mentioned criteria are not fulfilled.
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:
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
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
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:
We can make loading not fail if XML is not present and drop the file altogether
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)
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
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.
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']}")
will be nice to add this script to repo for speedup migration from older version to latest
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;
}
}`