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:
MagicBotandClaude Opus 5.5 committed 2026-10-22 11:19:14 -04:00
1 parent 623f7c4ae6
commit bd145b1e55
5 files changed
+497 -3

No files matched your search

@@ -0,0 +1,132 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects.wpak;
import engine.mbEnums;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
// One POWERBEGIN block of Powers.cfg (client loader 0x56E410).
// Header field names follow the client's "<field> Error with line" messages.
// KEY= lines map to the lower-case fields named after them.
//
// Read only once loaded.
public class PowerTemplate {
public static HashMap<String, PowerTemplate> _powers = new HashMap<>();
// Header: 22 tokens
public String power_id;
public String power_name;
public mbEnums.PowerType power_type;
public int icon_number;
public String focus;
public mbEnums.PowerTargetType target_type;
public float range;
public mbEnums.AreaType area_type;
public float area_range;
public mbEnums.ExcludeType exclude_type;
public mbEnums.CostType cost_type;
public float cost_amt;
public int difficulty;
public int precision;
public float init_time;
public float release_time;
public float recycle_time;
public boolean hit_roll_yn;
public mbEnums.CastingModeType casting_mode;
public int init_anim;
public int release_anim;
public mbEnums.TargetSelectType target_sel;
// Keys
public final ArrayList<ActionEntry> actions = new ArrayList<>();
public int maxlevel;
public float hatevalue;
public String hatevalue_curve;
public String grantoverridevar;
public mbEnums.PowerCategoryType category;
public final ArrayList<EquipPrereq> eqprereqs = new ArrayList<>();
public final ArrayList<mbEnums.MonsterType> monstertypeprereqs = new ArrayList<>();
public final ArrayList<mbEnums.MonsterType> monstertyperestricts = new ArrayList<>();
public final ArrayList<EffectPrereq> targeteffectprereqs_anded = new ArrayList<>();
public final ArrayList<EffectPrereq> targeteffectprereqs_ored = new ArrayList<>();
public final ArrayList<EffectPrereq> effectprereqs = new ArrayList<>();
// CURVE= <COSTAMT|INITTIME|RECYCLETIME> <curve>: the curve applied to that header value
public final LinkedHashMap<String, String> curves = new LinkedHashMap<>();
public final ArrayList<String> descriptions = new ArrayList<>();
public int loopanimid;
public boolean bladetrails;
public boolean cancastwhilemoving;
public boolean cancastwhileflying;
public boolean shouldcheckpath;
public boolean isadminpower;
public boolean cantargetadmin;
public boolean isprojectile;
public boolean sticky;
public int maxnummobtargets;
public int maxnumplayertargets;
public float pulseinfo_duration;
public int pulseinfo_iterations;
public final ArrayList<Integer> sounds = new ArrayList<>();
public int casterspulseparticle;
public final ArrayList<EffectTemplate.ConditionEntry> conditions = new ArrayList<>();
public String initstring;
public String successself;
public String successother;
public String fizzleself;
public String fizzleother;
public String applyeffectself;
public String applyeffectother;
public String wearoffeffectself;
public String wearoffeffectother;
public String applydamagecaster;
public String applydamagetarget;
public String applydamageother;
// ACTION= <action_id> <min_trains> <max_trains> <duration> <duration_curve> <stack_type>
// <stack_order> <stack_priority>, then up to four optional pairs kept only in args
public static class ActionEntry {
public String action_id;
public int min_trains;
public int max_trains;
public float duration;
public String duration_curve;
public String stack_type;
public int stack_order;
public mbEnums.CategoryToPowerType stack_priority;
public final ArrayList<String> args = new ArrayList<>();
}
// EQPREREQ= repeats <slot> <skill> <required>
public static class EquipPrereq {
public mbEnums.EquipSlotType slot;
public String skill;
public boolean required;
}
public static class EffectPrereq {
public String effect_id;
public int value;
public String message;
}
}
+1 -1
View File
@@ -208,7 +208,7 @@ public class EffectParser {
mod.curve = WpakParserUtil.curveToken(line, 3);
}
private static EffectTemplate.ConditionEntry parseCondition(WpakLine line) {
static EffectTemplate.ConditionEntry parseCondition(WpakLine line) {
EffectTemplate.ConditionEntry condition = new EffectTemplate.ConditionEntry();
+330
View File
@@ -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;
}
}
+24 -2
View File
@@ -9,14 +9,22 @@
package engine.parsers.wpak;
import engine.parsers.cache.CacheParserUtil;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// One non-comment line of a wpak cfg file, split into tokens.
// Every accessor that fails names the file and line.
public class WpakLine {
// The client reads numbers like C atoi/atof: the leading number counts, the rest is dropped
// (Powers.cfg has "2.9.0" for a float and "10.0" for an int)
private static final Pattern intPrefix = Pattern.compile("[-+]?\\d+");
private static final Pattern floatPrefix = Pattern.compile("[-+]?(\\d+\\.?\\d*|\\.\\d+)");
public final String fileName;
public final int lineNumber;
public final String text;
@@ -56,23 +64,37 @@ public class WpakLine {
public int intToken(int index) {
String value = token(index);
Matcher prefix;
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw error("token " + index + " '" + value + "' is not an integer");
prefix = intPrefix.matcher(value);
}
if (!prefix.lookingAt())
throw error("token " + index + " '" + value + "' is not an integer");
Logger.warn(location() + ": token " + index + " '" + value + "' read as " + prefix.group() + ", as the client does");
return Integer.parseInt(prefix.group());
}
public float floatToken(int index) {
String value = token(index);
Matcher prefix;
try {
return Float.parseFloat(value);
} catch (NumberFormatException e) {
throw error("token " + index + " '" + value + "' is not a number");
prefix = floatPrefix.matcher(value);
}
if (!prefix.lookingAt())
throw error("token " + index + " '" + value + "' is not a number");
Logger.warn(location() + ": token " + index + " '" + value + "' read as " + prefix.group() + ", as the client does");
return Float.parseFloat(prefix.group());
}
// The client reads a missing trailing number as 0 (String::ToInt on an empty token)
+10
View File
@@ -43,10 +43,12 @@ import engine.parsers.objects.wpak.CompoundCurveTemplate;
import engine.parsers.objects.wpak.EffectTemplate;
import engine.parsers.objects.wpak.ModTypeTemplate;
import engine.parsers.objects.wpak.PowerActionTemplate;
import engine.parsers.objects.wpak.PowerTemplate;
import engine.parsers.wpak.CompoundCurveParser;
import engine.parsers.wpak.EffectParser;
import engine.parsers.wpak.ModTypeParser;
import engine.parsers.wpak.PowerActionParser;
import engine.parsers.wpak.PowerParser;
import engine.powers.RunePowerEntry;
import engine.powers.RuneSkillAdjustEntry;
import engine.server.MBServerStatics;
@@ -374,6 +376,14 @@ public class WorldServer {
exit(1);
}
Logger.info("Loading Powers");
PowerTemplate._powers = PowerParser.parsePowersFile();
if (PowerTemplate._powers == null || PowerTemplate._powers.isEmpty()) {
Logger.error("ABORT! Powers failed to load!");
exit(1);
}
Logger.info("Loading PromotionClasses");
PromotionClass.LoadAllPromotions();