// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // 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 clientIgnoredKeys = new HashSet<>(Arrays.asList( "APPLYDAMAGESELF=", "APPLYEFFECTCASTER=", "APPLYEFFECTTARGET=", "POWEREND=")); public static HashMap parsePowersFile() { HashMap 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 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 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 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; } }