forked from MagicBane/Server
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>
331 lines
13 KiB
Java
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;
|
|
}
|
|
}
|