forked from MagicBane/Server
Load Powers.cfg at world boot
PowerParser loads Powers.cfg into PowerTemplate._powers. The 22 header fields are named after the client loader's error messages (0x56E410); ACTION= lines keep trains, duration, curve and stack fields; prereqs, curves, flags, messages and CONDITION blocks are kept. Keys compare case-insensitively, as in the client. Four keys the client ignores (APPLYDAMAGESELF=, APPLYEFFECTCASTER=, APPLYEFFECTTARGET= and a POWEREND= typo) are skipped; any other unknown key fails the load. Numbers are read like the client's atoi/atof with a warning when a token isn't clean: PRL-033 init time "2.9.0" -> 2.9 and SGE-001 precision "10.0" -> 10, which matches the DB. Ten EQPREREQ lines end without a required flag, which the client reads as 0. Needs the fixed Powers.cfg (SRDX-DB header had "SPELL 15 Unknown" pasted twice; fixed in MBDATA, findings section 6). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
623f7c4ae6
commit
bd145b1e55
5 files changed
+497
-3
No files matched your search
@@ -0,0 +1,330 @@
|
||||
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
|
||||
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
|
||||
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
|
||||
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
|
||||
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
|
||||
// Magicbane Emulator Project © 2013 - 2026
|
||||
// https://www.magicbane.com
|
||||
|
||||
package engine.parsers.wpak;
|
||||
|
||||
import engine.mbEnums;
|
||||
import engine.parsers.objects.wpak.PowerTemplate;
|
||||
import org.pmw.tinylog.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
||||
// Loads Powers.cfg (client loader 0x56E410). CompoundCurves.cfg must be loaded first.
|
||||
|
||||
public class PowerParser {
|
||||
|
||||
private static final String fileName = "Powers.cfg";
|
||||
private static final int headerSize = 22;
|
||||
|
||||
// Keys the client's loader doesn't know and skips; "POWEREND=" is a typo for CURVE= on one power
|
||||
private static final HashSet<String> clientIgnoredKeys = new HashSet<>(Arrays.asList(
|
||||
"APPLYDAMAGESELF=", "APPLYEFFECTCASTER=", "APPLYEFFECTTARGET=", "POWEREND="));
|
||||
|
||||
public static HashMap<String, PowerTemplate> parsePowersFile() {
|
||||
|
||||
HashMap<String, PowerTemplate> powers = new HashMap<>();
|
||||
WpakBlock root;
|
||||
PowerTemplate power;
|
||||
int blockCount = 0;
|
||||
int failedCount = 0;
|
||||
|
||||
try {
|
||||
root = WpakFileReader.readBlocks(fileName);
|
||||
} catch (Exception e) {
|
||||
Logger.error(fileName + ": " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
for (WpakLine line : root.lines()) {
|
||||
Logger.error(line.location() + ": line outside POWERBEGIN/END");
|
||||
failedCount++;
|
||||
}
|
||||
|
||||
for (WpakBlock block : root.blocks()) {
|
||||
try {
|
||||
if (!block.name.equals("POWER"))
|
||||
throw block.beginLine.error("unexpected " + block.name + "BEGIN");
|
||||
|
||||
blockCount++;
|
||||
power = parsePowerBlock(block);
|
||||
|
||||
if (powers.containsKey(power.power_id))
|
||||
Logger.warn(block.location() + ": duplicate power " + power.power_id + "; the later entry wins");
|
||||
|
||||
powers.put(power.power_id, power);
|
||||
} catch (WpakFormatException e) {
|
||||
Logger.error(e.getMessage());
|
||||
failedCount++;
|
||||
} catch (Exception e) {
|
||||
Logger.error(block.location() + ": " + e);
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info("Loaded " + powers.size() + " of " + blockCount + " powers");
|
||||
|
||||
if (failedCount > 0)
|
||||
return null;
|
||||
|
||||
return powers;
|
||||
}
|
||||
|
||||
private static PowerTemplate parsePowerBlock(WpakBlock block) {
|
||||
|
||||
PowerTemplate power = new PowerTemplate();
|
||||
ArrayList<WpakLine> lines = block.lines();
|
||||
|
||||
if (lines.isEmpty())
|
||||
throw block.beginLine.error("power has no header line");
|
||||
|
||||
parseHeader(power, lines.get(0));
|
||||
|
||||
for (int i = 1; i < lines.size(); i++)
|
||||
parseKey(power, lines.get(i));
|
||||
|
||||
for (WpakBlock child : block.blocks()) {
|
||||
if (!child.name.equals("CONDITION"))
|
||||
throw child.beginLine.error("unexpected " + child.name + "BEGIN in a power");
|
||||
|
||||
for (WpakLine line : child.lines())
|
||||
power.conditions.add(EffectParser.parseCondition(line));
|
||||
}
|
||||
|
||||
return power;
|
||||
}
|
||||
|
||||
private static void parseHeader(PowerTemplate power, WpakLine header) {
|
||||
|
||||
if (header.size() != headerSize)
|
||||
throw header.error("power header has " + header.size() + " tokens, expected " + headerSize);
|
||||
|
||||
power.power_id = header.token(0);
|
||||
power.power_name = header.token(1);
|
||||
power.power_type = header.enumToken(mbEnums.PowerType.class, 2);
|
||||
power.icon_number = header.intToken(3);
|
||||
power.focus = header.token(4);
|
||||
power.target_type = header.enumToken(mbEnums.PowerTargetType.class, 5);
|
||||
power.range = header.floatToken(6);
|
||||
power.area_type = header.enumToken(mbEnums.AreaType.class, 7);
|
||||
power.area_range = header.floatToken(8);
|
||||
power.exclude_type = header.enumToken(mbEnums.ExcludeType.class, 9);
|
||||
power.cost_type = header.enumToken(mbEnums.CostType.class, 10);
|
||||
power.cost_amt = header.floatToken(11);
|
||||
power.difficulty = header.intToken(12);
|
||||
power.precision = header.intToken(13);
|
||||
power.init_time = header.floatToken(14);
|
||||
power.release_time = header.floatToken(15);
|
||||
power.recycle_time = header.floatToken(16);
|
||||
power.hit_roll_yn = header.boolToken(17);
|
||||
power.casting_mode = header.enumToken(mbEnums.CastingModeType.class, 18);
|
||||
power.init_anim = header.intToken(19);
|
||||
power.release_anim = header.intToken(20);
|
||||
power.target_sel = header.enumToken(mbEnums.TargetSelectType.class, 21);
|
||||
}
|
||||
|
||||
// The client compares keys case-insensitively (cfg has MaxLevel= and HateValue=)
|
||||
|
||||
private static void parseKey(PowerTemplate power, WpakLine line) {
|
||||
|
||||
String key = line.token(0).toUpperCase();
|
||||
|
||||
switch (key) {
|
||||
case "ACTION=":
|
||||
power.actions.add(parseAction(line));
|
||||
break;
|
||||
case "MAXLEVEL=":
|
||||
power.maxlevel = line.intToken(1);
|
||||
break;
|
||||
case "HATEVALUE=":
|
||||
power.hatevalue = line.floatToken(1);
|
||||
power.hatevalue_curve = line.has(2) ? WpakParserUtil.curveToken(line, 2) : null;
|
||||
break;
|
||||
case "GRANTOVERRIDEVAR=":
|
||||
power.grantoverridevar = line.token(1);
|
||||
break;
|
||||
case "CATEGORY=":
|
||||
power.category = line.enumToken(mbEnums.PowerCategoryType.class, 1);
|
||||
break;
|
||||
case "EQPREREQ=":
|
||||
parseEquipPrereqs(power, line);
|
||||
break;
|
||||
case "MONSTERTYPEPREREQS=":
|
||||
for (int i = 1; i < line.size(); i++)
|
||||
power.monstertypeprereqs.add(line.enumToken(mbEnums.MonsterType.class, i));
|
||||
break;
|
||||
case "MONSTERTYPERESTRICTS=":
|
||||
for (int i = 1; i < line.size(); i++)
|
||||
power.monstertyperestricts.add(line.enumToken(mbEnums.MonsterType.class, i));
|
||||
break;
|
||||
case "TARGETEFFECTPREREQS_ANDED=":
|
||||
parseEffectPrereqPairs(power.targeteffectprereqs_anded, line);
|
||||
break;
|
||||
case "TARGETEFFECTPREREQS_ORED=":
|
||||
parseEffectPrereqPairs(power.targeteffectprereqs_ored, line);
|
||||
break;
|
||||
case "EFFECTPREREQ=":
|
||||
power.effectprereqs.add(parseEffectPrereq(line));
|
||||
break;
|
||||
case "CURVE=":
|
||||
power.curves.put(line.token(1), WpakParserUtil.curveToken(line, 2));
|
||||
break;
|
||||
case "DESCRIPTION=":
|
||||
power.descriptions.add(line.token(1));
|
||||
break;
|
||||
case "LOOPANIMID=":
|
||||
power.loopanimid = line.intToken(1);
|
||||
break;
|
||||
case "BLADETRAILS=":
|
||||
power.bladetrails = line.optionalBoolToken(1);
|
||||
break;
|
||||
case "CANCASTWHILEMOVING=":
|
||||
power.cancastwhilemoving = line.optionalBoolToken(1);
|
||||
break;
|
||||
case "CANCASTWHILEFLYING=":
|
||||
power.cancastwhileflying = line.optionalBoolToken(1);
|
||||
break;
|
||||
case "SHOULDCHECKPATH=":
|
||||
power.shouldcheckpath = line.optionalBoolToken(1);
|
||||
break;
|
||||
case "ISADMINPOWER=":
|
||||
power.isadminpower = line.optionalBoolToken(1);
|
||||
break;
|
||||
case "CANTARGETADMIN=":
|
||||
power.cantargetadmin = line.optionalBoolToken(1);
|
||||
break;
|
||||
case "ISPROJECTILE=":
|
||||
power.isprojectile = line.optionalBoolToken(1);
|
||||
break;
|
||||
case "STICKY=":
|
||||
power.sticky = line.optionalBoolToken(1);
|
||||
break;
|
||||
case "MAXNUMMOBTARGETS=":
|
||||
power.maxnummobtargets = line.intToken(1);
|
||||
break;
|
||||
case "MAXNUMPLAYERTARGETS=":
|
||||
power.maxnumplayertargets = line.intToken(1);
|
||||
break;
|
||||
case "PULSEINFO=":
|
||||
power.pulseinfo_duration = line.floatToken(1);
|
||||
power.pulseinfo_iterations = line.intToken(2);
|
||||
break;
|
||||
case "SOUNDS=":
|
||||
power.sounds.addAll(line.intTokensFrom(1));
|
||||
break;
|
||||
case "CASTERSPULSEPARTICLE=":
|
||||
power.casterspulseparticle = line.intToken(1);
|
||||
break;
|
||||
case "INITSTRING=":
|
||||
power.initstring = line.token(1);
|
||||
break;
|
||||
case "SUCCESSSELF=":
|
||||
power.successself = line.token(1);
|
||||
break;
|
||||
case "SUCCESSOTHER=":
|
||||
power.successother = line.token(1);
|
||||
break;
|
||||
case "FIZZLESELF=":
|
||||
power.fizzleself = line.token(1);
|
||||
break;
|
||||
case "FIZZLEOTHER=":
|
||||
power.fizzleother = line.token(1);
|
||||
break;
|
||||
case "APPLYEFFECTSELF=":
|
||||
power.applyeffectself = line.token(1);
|
||||
break;
|
||||
case "APPLYEFFECTOTHER=":
|
||||
power.applyeffectother = line.token(1);
|
||||
break;
|
||||
case "WEAROFFEFFECTSELF=":
|
||||
power.wearoffeffectself = line.token(1);
|
||||
break;
|
||||
case "WEAROFFEFFECTOTHER=":
|
||||
power.wearoffeffectother = line.token(1);
|
||||
break;
|
||||
case "APPLYDAMAGECASTER=":
|
||||
power.applydamagecaster = line.token(1);
|
||||
break;
|
||||
case "APPLYDAMAGETARGET=":
|
||||
power.applydamagetarget = line.token(1);
|
||||
break;
|
||||
case "APPLYDAMAGEOTHER=":
|
||||
power.applydamageother = line.token(1);
|
||||
break;
|
||||
default:
|
||||
if (!clientIgnoredKeys.contains(key))
|
||||
throw line.error("unknown power key " + line.token(0));
|
||||
}
|
||||
}
|
||||
|
||||
private static PowerTemplate.ActionEntry parseAction(WpakLine line) {
|
||||
|
||||
PowerTemplate.ActionEntry action = new PowerTemplate.ActionEntry();
|
||||
|
||||
action.action_id = line.token(1);
|
||||
action.min_trains = line.intToken(2);
|
||||
action.max_trains = line.intToken(3);
|
||||
action.duration = line.floatToken(4);
|
||||
action.duration_curve = WpakParserUtil.curveToken(line, 5);
|
||||
action.stack_type = line.token(6);
|
||||
action.stack_order = line.intToken(7);
|
||||
action.stack_priority = line.enumToken(mbEnums.CategoryToPowerType.class, 8);
|
||||
action.args.addAll(line.tokens.subList(1, line.size()));
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
// Ten lines end in "LHELD Bow" with no flag; the client reads the missing flag as 0
|
||||
|
||||
private static void parseEquipPrereqs(PowerTemplate power, WpakLine line) {
|
||||
|
||||
PowerTemplate.EquipPrereq prereq;
|
||||
|
||||
for (int i = 1; i < line.size(); i += 3) {
|
||||
prereq = new PowerTemplate.EquipPrereq();
|
||||
prereq.slot = line.enumToken(mbEnums.EquipSlotType.class, i);
|
||||
prereq.skill = line.token(i + 1);
|
||||
prereq.required = line.optionalBoolToken(i + 2);
|
||||
power.eqprereqs.add(prereq);
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseEffectPrereqPairs(ArrayList<PowerTemplate.EffectPrereq> prereqs, WpakLine line) {
|
||||
|
||||
PowerTemplate.EffectPrereq prereq;
|
||||
|
||||
for (int i = 1; i < line.size(); i += 2) {
|
||||
prereq = new PowerTemplate.EffectPrereq();
|
||||
prereq.effect_id = line.token(i);
|
||||
prereq.value = line.intToken(i + 1);
|
||||
prereqs.add(prereq);
|
||||
}
|
||||
}
|
||||
|
||||
// The message is the rest of the line; two lines wrap it in doubled quotes (""text""),
|
||||
// which split it into words with empty tokens at each end
|
||||
|
||||
private static PowerTemplate.EffectPrereq parseEffectPrereq(WpakLine line) {
|
||||
|
||||
PowerTemplate.EffectPrereq prereq = new PowerTemplate.EffectPrereq();
|
||||
ArrayList<String> words = new ArrayList<>();
|
||||
|
||||
prereq.effect_id = line.token(1);
|
||||
prereq.value = line.intToken(2);
|
||||
|
||||
for (int i = 3; i < line.size(); i++)
|
||||
if (!line.token(i).isEmpty())
|
||||
words.add(line.token(i));
|
||||
|
||||
prereq.message = String.join(" ", words);
|
||||
|
||||
return prereq;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user