From 6c820940d340ed7008878cc2b95215eb43a7b731 Mon Sep 17 00:00:00 2001 From: MagicBot Date: Thu, 22 Oct 2026 11:08:09 -0400 Subject: [PATCH] Add wpak cfg reader and PowerActions.cfg loader New engine.parsers.wpak importer (replaces engine.wpak, which stays unused). WpakFileReader tokenizes cfg lines (quotes, '#'/'*' comments, trailing '#') and builds XxxBEGIN/XxxEND blocks; every error names file:line. PowerActionParser loads PowerActions.cfg into PowerActionTemplate._powerActions, keyed by action id, at world boot. Header layouts per action type come from the client's ArcPowerAction*_ParseHeader functions: the trailing 0/1 on ApplyEffect and friends is the aggressive flag, not a level. effect_ids holds the action -> effect map needed by Task 2.10b. Three ids (PRE-235..237) repeat identically; the later entry wins, as in the client. No consumers change. Co-Authored-By: Claude Opus 5.5 --- .../objects/wpak/PowerActionTemplate.java | 86 ++++++ .../parsers/wpak/PowerActionParser.java | 257 ++++++++++++++++++ src/engine/parsers/wpak/WpakBlock.java | 52 ++++ src/engine/parsers/wpak/WpakFileReader.java | 152 +++++++++++ .../parsers/wpak/WpakFormatException.java | 18 ++ src/engine/parsers/wpak/WpakLine.java | 136 +++++++++ src/engine/server/world/WorldServer.java | 10 + 7 files changed, 711 insertions(+) create mode 100644 src/engine/parsers/objects/wpak/PowerActionTemplate.java create mode 100644 src/engine/parsers/wpak/PowerActionParser.java create mode 100644 src/engine/parsers/wpak/WpakBlock.java create mode 100644 src/engine/parsers/wpak/WpakFileReader.java create mode 100644 src/engine/parsers/wpak/WpakFormatException.java create mode 100644 src/engine/parsers/wpak/WpakLine.java diff --git a/src/engine/parsers/objects/wpak/PowerActionTemplate.java b/src/engine/parsers/objects/wpak/PowerActionTemplate.java new file mode 100644 index 00000000..232263a9 --- /dev/null +++ b/src/engine/parsers/objects/wpak/PowerActionTemplate.java @@ -0,0 +1,86 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// 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; + +// One POWERACTIONBEGIN block of PowerActions.cfg. +// +// Header: . The per-type layouts +// come from the client's ArcPowerAction*_ParseHeader functions. header_args keeps +// every arg as written, including ones the client reads but nothing names yet. +// KEY= lines map to the lower-case fields named after them. +// +// Read only once loaded. + +public class PowerActionTemplate { + + public static HashMap _powerActions = new HashMap<>(); + + public String action_id; + public mbEnums.PowerActionType action_type; + public final ArrayList header_args = new ArrayList<>(); + + // Header + + // ApplyEffect, ApplyEffects, Block, Charm, DamageOverTime, DeferredPower, + // DirectDamage, Invis, SpireDisable, Track, Transform, TransferStatOT + public final ArrayList effect_ids = new ArrayList<>(); + + public String deferred_power_id; + public int num_iterations; + public int mob_id; + public int mob_level; + public String effect_source_to_remove; + public mbEnums.ItemFlags item_flag; + public boolean item_flag_value; + public String track_filter; + public int max_track; + + public mbEnums.CostType transfer_from_type; + public mbEnums.CostType transfer_to_type; + + // The client reads both with String::ToInt, so 31.5 acts as 31 (0x56ADF0) + public float transfer_amount; + public String transfer_amount_curve; + public float transfer_efficiency; + public String transfer_efficiency_curve; + + // Keys, plus the header's trailing aggressive flag (both set the same client field) + + public boolean isaggressive; + public boolean bladetrails; + public boolean isresistable; + public String applyeffectself; + public String applyeffectother; + public String wearoffeffectself; + public String wearoffeffectother; + public boolean applyeffectblank; + public boolean wearoffeffectblank; + public boolean is_available_for_formulae; + public boolean removeall; + public final ArrayList bodyparts = new ArrayList<>(); + public final ArrayList femalebodyparts = new ArrayList<>(); + public boolean shouldshowweapons; + public boolean shouldshowarmor; + public final ArrayList attackanims = new ArrayList<>(); + public float[] scalefactor; + public String effectid; + public int levelcap; + public String levelcap_curve; + public boolean clearaggro; + public boolean targetbecomespet; + public boolean destroyoldpet; + public String rootfsmid; + public mbEnums.DamageType damagetype; + public final ArrayList splashdamage = new ArrayList<>(); +} diff --git a/src/engine/parsers/wpak/PowerActionParser.java b/src/engine/parsers/wpak/PowerActionParser.java new file mode 100644 index 00000000..7fc6e796 --- /dev/null +++ b/src/engine/parsers/wpak/PowerActionParser.java @@ -0,0 +1,257 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.wpak; + +import engine.mbEnums; +import engine.parsers.objects.wpak.PowerActionTemplate; +import org.pmw.tinylog.Logger; + +import java.util.ArrayList; +import java.util.HashMap; + +// Loads PowerActions.cfg (client loader PowerActions_LoadCfg 0x578EC0). + +public class PowerActionParser { + + private static final String fileName = "PowerActions.cfg"; + + public static HashMap parsePowerActionsFile() { + + HashMap powerActions = new HashMap<>(); + WpakBlock root; + PowerActionTemplate powerAction; + 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 POWERACTIONBEGIN/END"); + failedCount++; + } + + for (WpakBlock block : root.blocks()) { + try { + if (!block.name.equals("POWERACTION")) + throw block.beginLine.error("unexpected " + block.name + "BEGIN"); + + blockCount++; + powerAction = parsePowerActionBlock(block); + + // The client's map assigns on insert, so a repeated id keeps the later entry + if (powerActions.containsKey(powerAction.action_id)) + Logger.warn(block.location() + ": duplicate power action " + powerAction.action_id + "; the later entry wins"); + + powerActions.put(powerAction.action_id, powerAction); + } catch (WpakFormatException e) { + Logger.error(e.getMessage()); + failedCount++; + } catch (Exception e) { + Logger.error(block.location() + ": " + e); + failedCount++; + } + } + + Logger.info("Loaded " + powerActions.size() + " of " + blockCount + " power actions"); + + if (failedCount > 0) + return null; + + return powerActions; + } + + private static PowerActionTemplate parsePowerActionBlock(WpakBlock block) { + + PowerActionTemplate powerAction = new PowerActionTemplate(); + ArrayList lines = block.lines(); + WpakLine header; + + if (!block.blocks().isEmpty()) + throw block.blocks().get(0).beginLine.error("nested block in a power action"); + + if (lines.isEmpty()) + throw block.beginLine.error("empty power action"); + + header = lines.get(0); + powerAction.action_id = header.token(0); + powerAction.action_type = header.enumToken(mbEnums.PowerActionType.class, 1); + powerAction.header_args.addAll(header.tokens.subList(2, header.size())); + + parseHeader(powerAction, header); + + for (int i = 1; i < lines.size(); i++) + parseKey(powerAction, lines.get(i)); + + return powerAction; + } + + // Layouts from the ArcPowerAction*_ParseHeader functions; args start at token 2 + + private static void parseHeader(PowerActionTemplate powerAction, WpakLine header) { + + switch (powerAction.action_type) { + case ApplyEffect: + case Block: + case Charm: + case DirectDamage: + case Invis: + case SpireDisable: + powerAction.effect_ids.add(header.token(2)); + powerAction.isaggressive = header.optionalIntToken(3) != 0; + break; + case ApplyEffects: + powerAction.isaggressive = header.intToken(2) != 0; + for (int i = 3; i < header.size(); i++) + powerAction.effect_ids.add(header.token(i)); + break; + case DeferredPower: + powerAction.effect_ids.add(header.token(2)); + powerAction.isaggressive = header.intToken(3) != 0; + powerAction.deferred_power_id = header.token(4); + break; + case DamageOverTime: + powerAction.effect_ids.add(header.token(2)); + powerAction.num_iterations = header.intToken(3); + powerAction.isaggressive = header.optionalIntToken(4) != 0; + break; + case CreateMob: + powerAction.mob_id = header.intToken(2); + powerAction.mob_level = header.intToken(3); + break; + case RemoveEffect: + powerAction.effect_source_to_remove = header.token(2); + break; + case SetItemFlag: + powerAction.item_flag = header.enumToken(mbEnums.ItemFlags.class, 2); + powerAction.item_flag_value = header.optionalBoolToken(3); + break; + case Track: + powerAction.effect_ids.add(header.token(2)); + powerAction.track_filter = header.token(5); + powerAction.max_track = header.intToken(7); + break; + case Transform: + powerAction.effect_ids.add(header.token(2)); + break; + case TransferStat: + parseTransferStat(powerAction, header); + break; + case TransferStatOT: + parseTransferStat(powerAction, header); + powerAction.effect_ids.add(header.token(10)); + powerAction.num_iterations = header.intToken(11); + break; + default: + break; + } + } + + private static void parseTransferStat(PowerActionTemplate powerAction, WpakLine header) { + + powerAction.transfer_from_type = header.enumToken(mbEnums.CostType.class, 2); + powerAction.transfer_to_type = header.enumToken(mbEnums.CostType.class, 3); + powerAction.transfer_amount = header.floatToken(4); + powerAction.transfer_amount_curve = header.token(5); + powerAction.transfer_efficiency = header.floatToken(6); + powerAction.transfer_efficiency_curve = header.token(7); + powerAction.isaggressive = header.optionalIntToken(9) != 0; + } + + // Shared keys from ArcPowerAction_ParseKey 0x579820; the rest are per action type. + // The client logs an unknown key and goes on; here it fails the load so no data is dropped silently. + + private static void parseKey(PowerActionTemplate powerAction, WpakLine line) { + + switch (line.token(0)) { + case "ISAGGRESSIVE=": + powerAction.isaggressive = line.optionalBoolToken(1); + break; + case "BLADETRAILS=": + powerAction.bladetrails = line.optionalBoolToken(1); + break; + case "ISRESISTABLE=": + powerAction.isresistable = line.optionalBoolToken(1); + break; + case "APPLYEFFECTSELF=": + powerAction.applyeffectself = line.token(1); + break; + case "APPLYEFFECTOTHER=": + powerAction.applyeffectother = line.token(1); + break; + case "WEAROFFEFFECTSELF=": + powerAction.wearoffeffectself = line.token(1); + break; + case "WEAROFFEFFECTOTHER=": + powerAction.wearoffeffectother = line.token(1); + break; + case "APPLYEFFECTBLANK=": + powerAction.applyeffectblank = line.optionalBoolToken(1); + break; + case "WEAROFFEFFECTBLANK=": + powerAction.wearoffeffectblank = line.optionalBoolToken(1); + break; + case "IS_AVAILABLE_FOR_FORMULAE=": + powerAction.is_available_for_formulae = line.optionalBoolToken(1); + break; + case "REMOVEALL=": + powerAction.removeall = line.optionalBoolToken(1); + break; + case "BODYPARTS=": + powerAction.bodyparts.addAll(line.intTokensFrom(1)); + break; + case "FEMALEBODYPARTS=": + powerAction.femalebodyparts.addAll(line.intTokensFrom(1)); + break; + case "SHOULDSHOWWEAPONS=": + powerAction.shouldshowweapons = line.optionalBoolToken(1); + break; + case "SHOULDSHOWARMOR=": + powerAction.shouldshowarmor = line.optionalBoolToken(1); + break; + case "ATTACKANIMS=": + powerAction.attackanims.addAll(line.intTokensFrom(1)); + break; + case "SCALEFACTOR=": + powerAction.scalefactor = new float[]{line.floatToken(1), line.floatToken(2), line.floatToken(3)}; + break; + case "EFFECTID=": + powerAction.effectid = line.token(1); + break; + case "LEVELCAP=": + powerAction.levelcap = line.intToken(1); + powerAction.levelcap_curve = line.has(2) ? line.token(2) : null; + break; + case "CLEARAGGRO=": + powerAction.clearaggro = line.optionalBoolToken(1); + break; + case "TARGETBECOMESPET=": + powerAction.targetbecomespet = line.optionalBoolToken(1); + break; + case "DESTROYOLDPET=": + powerAction.destroyoldpet = line.optionalBoolToken(1); + break; + case "ROOTFSMID=": + powerAction.rootfsmid = line.token(1); + break; + case "DAMAGETYPE=": + powerAction.damagetype = line.enumToken(mbEnums.DamageType.class, 1); + break; + case "SPLASHDAMAGE=": + powerAction.splashdamage.addAll(line.intTokensFrom(1)); + break; + default: + throw line.error("unknown power action key " + line.token(0)); + } + } +} diff --git a/src/engine/parsers/wpak/WpakBlock.java b/src/engine/parsers/wpak/WpakBlock.java new file mode 100644 index 00000000..f51eb4d0 --- /dev/null +++ b/src/engine/parsers/wpak/WpakBlock.java @@ -0,0 +1,52 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.wpak; + +import java.util.ArrayList; + +// A XxxBEGIN/XxxEND block (name "Xxx"), or the whole file for the root block. +// Lines and nested blocks keep their file order in children. + +public class WpakBlock { + + public final String name; + public final WpakLine beginLine; + public final ArrayList children = new ArrayList<>(); + + public WpakBlock(String name, WpakLine beginLine) { + this.name = name; + this.beginLine = beginLine; + } + + public String location() { + return beginLine == null ? name : beginLine.location(); + } + + public ArrayList lines() { + + ArrayList lines = new ArrayList<>(); + + for (Object child : children) + if (child instanceof WpakLine) + lines.add((WpakLine) child); + + return lines; + } + + public ArrayList blocks() { + + ArrayList blocks = new ArrayList<>(); + + for (Object child : children) + if (child instanceof WpakBlock) + blocks.add((WpakBlock) child); + + return blocks; + } +} diff --git a/src/engine/parsers/wpak/WpakFileReader.java b/src/engine/parsers/wpak/WpakFileReader.java new file mode 100644 index 00000000..bc5771c7 --- /dev/null +++ b/src/engine/parsers/wpak/WpakFileReader.java @@ -0,0 +1,152 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.wpak; + +import engine.gameManager.ConfigManager; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; + +// Reads the client's wpak cfg files (an mbEditorPro export of Config.wpak) into +// tokenized lines and XxxBEGIN/XxxEND blocks. Each parser maps the tokens itself. + +public class WpakFileReader { + + public static final String wpakPath = ConfigManager.DEFAULT_DATA_DIR + "wpak/"; + + // Lines starting with '#' or '*' are comments (PowerActions_LoadCfg 0x578EC0). + // A '#' outside quotes ends the line, as in PowActionCostInfo.cfg. + + public static ArrayList readLines(String fileName) throws IOException { + + ArrayList lines = new ArrayList<>(); + List fileLines = Files.readAllLines(Paths.get(wpakPath + fileName), StandardCharsets.ISO_8859_1); + String text; + String trimmed; + ArrayList tokens; + + for (int i = 0; i < fileLines.size(); i++) { + text = fileLines.get(i); + trimmed = text.trim(); + + if (trimmed.isEmpty() || trimmed.charAt(0) == '#' || trimmed.charAt(0) == '*') + continue; + + tokens = tokenize(fileName, i + 1, text); + + if (!tokens.isEmpty()) + lines.add(new WpakLine(fileName, i + 1, text, tokens)); + } + + return lines; + } + + // A one-token line XxxBEGIN opens block "Xxx"; XxxEND closes the innermost one. + + public static WpakBlock readBlocks(String fileName) throws IOException { + + WpakBlock root = new WpakBlock(fileName, null); + ArrayDeque openBlocks = new ArrayDeque<>(); + WpakBlock block; + String token; + + openBlocks.push(root); + + for (WpakLine line : readLines(fileName)) { + token = line.token(0); + + if (line.size() == 1 && token.length() > 5 && token.endsWith("BEGIN")) { + block = new WpakBlock(token.substring(0, token.length() - 5), line); + openBlocks.peek().children.add(block); + openBlocks.push(block); + continue; + } + + if (line.size() == 1 && token.equals(openBlocks.peek().name + "END")) { + openBlocks.pop(); + continue; + } + + if (line.size() == 1 && closesOuterBlock(openBlocks, token)) + throw line.error(token + " before " + openBlocks.peek().name + "END (opened at " + openBlocks.peek().location() + ")"); + + openBlocks.peek().children.add(line); + } + + if (openBlocks.size() > 1) + throw new WpakFormatException(openBlocks.peek().location() + ": " + openBlocks.peek().name + "BEGIN is never closed"); + + return root; + } + + private static boolean closesOuterBlock(ArrayDeque openBlocks, String token) { + + for (WpakBlock block : openBlocks) + if (block.beginLine != null && token.equals(block.name + "END")) + return true; + + return false; + } + + // Whitespace separates tokens; double quotes group one token (quotes removed, no escapes). + + private static ArrayList tokenize(String fileName, int lineNumber, String text) { + + ArrayList tokens = new ArrayList<>(); + StringBuilder token = new StringBuilder(); + boolean inToken = false; + boolean inQuotes = false; + char c; + + for (int i = 0; i < text.length(); i++) { + c = text.charAt(i); + + if (inQuotes) { + if (c == '"') { + inQuotes = false; + tokens.add(token.toString()); + token.setLength(0); + } else + token.append(c); + continue; + } + + if (c == '"' || c == '#' || Character.isWhitespace(c)) { + if (inToken) { + tokens.add(token.toString()); + token.setLength(0); + inToken = false; + } + + if (c == '#') + break; + + if (c == '"') + inQuotes = true; + continue; + } + + token.append(c); + inToken = true; + } + + if (inQuotes) + throw new WpakFormatException(fileName + ":" + lineNumber + ": unclosed quote"); + + if (inToken) + tokens.add(token.toString()); + + return tokens; + } +} diff --git a/src/engine/parsers/wpak/WpakFormatException.java b/src/engine/parsers/wpak/WpakFormatException.java new file mode 100644 index 00000000..b36eba48 --- /dev/null +++ b/src/engine/parsers/wpak/WpakFormatException.java @@ -0,0 +1,18 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.wpak; + +// A malformed wpak cfg entry. The message starts with file:line. + +public class WpakFormatException extends RuntimeException { + + public WpakFormatException(String message) { + super(message); + } +} diff --git a/src/engine/parsers/wpak/WpakLine.java b/src/engine/parsers/wpak/WpakLine.java new file mode 100644 index 00000000..f3524f05 --- /dev/null +++ b/src/engine/parsers/wpak/WpakLine.java @@ -0,0 +1,136 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.wpak; + +import engine.parsers.cache.CacheParserUtil; + +import java.util.ArrayList; + +// One non-comment line of a wpak cfg file, split into tokens. +// Every accessor that fails names the file and line. + +public class WpakLine { + + public final String fileName; + public final int lineNumber; + public final String text; + public final ArrayList tokens; + + public WpakLine(String fileName, int lineNumber, String text, ArrayList tokens) { + this.fileName = fileName; + this.lineNumber = lineNumber; + this.text = text; + this.tokens = tokens; + } + + public String location() { + return fileName + ":" + lineNumber; + } + + public WpakFormatException error(String message) { + return new WpakFormatException(location() + ": " + message); + } + + public int size() { + return tokens.size(); + } + + public boolean has(int index) { + return index < tokens.size(); + } + + public String token(int index) { + + if (!has(index)) + throw error("missing token " + index + " in '" + text.trim() + "'"); + + return tokens.get(index); + } + + public int intToken(int index) { + + String value = token(index); + + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw error("token " + index + " '" + value + "' is not an integer"); + } + } + + public float floatToken(int index) { + + String value = token(index); + + try { + return Float.parseFloat(value); + } catch (NumberFormatException e) { + throw error("token " + index + " '" + value + "' is not a number"); + } + } + + // The client reads a missing trailing number as 0 (String::ToInt on an empty token) + + public int optionalIntToken(int index) { + + if (!has(index)) + return 0; + + return intToken(index); + } + + // TRUE/FALSE, or a number where non-zero is true + + public boolean boolToken(int index) { + + String value = token(index); + + if (value.equalsIgnoreCase("TRUE")) + return true; + + if (value.equalsIgnoreCase("FALSE")) + return false; + + try { + return Integer.parseInt(value) != 0; + } catch (NumberFormatException e) { + throw error("token " + index + " '" + value + "' is not a boolean"); + } + } + + public boolean optionalBoolToken(int index) { + + if (!has(index)) + return false; + + return boolToken(index); + } + + public ArrayList intTokensFrom(int index) { + + ArrayList values = new ArrayList<>(); + + for (int i = index; i < tokens.size(); i++) + values.add(intToken(i)); + + return values; + } + + public > E enumToken(Class enumClass, int index) { + + String value = token(index); + String name = CacheParserUtil.enumName(value); + + for (E constant : enumClass.getEnumConstants()) + if (constant.name().equalsIgnoreCase(name)) + return constant; + + throw error("token " + index + " '" + value + "' has no " + enumClass.getSimpleName()); + } +} diff --git a/src/engine/server/world/WorldServer.java b/src/engine/server/world/WorldServer.java index 58684a75..34f3d551 100644 --- a/src/engine/server/world/WorldServer.java +++ b/src/engine/server/world/WorldServer.java @@ -39,6 +39,8 @@ import engine.parsers.cache.RuneParser; import engine.parsers.objects.DeedTemplate; import engine.parsers.objects.ItemTemplate; import engine.parsers.objects.RuneTemplate; +import engine.parsers.objects.wpak.PowerActionTemplate; +import engine.parsers.wpak.PowerActionParser; import engine.powers.RunePowerEntry; import engine.powers.RuneSkillAdjustEntry; import engine.server.MBServerStatics; @@ -334,6 +336,14 @@ public class WorldServer { Logger.info("Indexing Cache Templates"); CObjectIndex.build(); + Logger.info("Loading Power Actions"); + PowerActionTemplate._powerActions = PowerActionParser.parsePowerActionsFile(); + + if (PowerActionTemplate._powerActions == null || PowerActionTemplate._powerActions.isEmpty()) { + Logger.error("ABORT! Power actions failed to load!"); + exit(1); + } + Logger.info("Loading PromotionClasses"); PromotionClass.LoadAllPromotions();