Files
BattleBane/src/engine/parsers/wpak/PowerParser.java
T
MagicBotandClaude Opus 5.5 098f5d2c38 Read wpak cfgs from one folder per archive
Files now load from mb.data/wpak/<archive>/<file>, matching MBDATA's
data/wpak layout and the client's Config/Config.wpak naming, so more
archives can sit beside Config/ later. All nine current files are in
wpak/Config/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:38:07 -04:00

331 lines
13 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// 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 = "Config/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;
}
}