feat: implement hardcore hearts with game mode lock

Display hardcore heart textures when a world is in hardcore mode,
matching Java Edition behavior. Hearts switch between normal/hardcore
across all states (poison, wither, flash) and all HUD resolutions.

C++ changes:
- IUIScene_HUD: check isHardcore() and call SetHardcoreMode() each tick
- UIScene_HUD: send hardcore boolean to Flash via Iggy, invalidate
  SetHealth dirty check on state change to force heart redraw
- CreateWorldMenu/LoadMenu: lock game mode to Survival when hardcore
- MinecraftServer: gate server.properties hardcore override behind
  MINECRAFT_SERVER_BUILD so offline worlds preserve their saved flag

SWF changes (via new Java tools):
- AddHardcoreBitmaps: adds 10 hardcore heart bitmaps to graphics SWFs
- AddHardcoreHearts: adds 10 new frames (15-24) to health sprite
- PatchHudABC: patches HUD ActionScript bytecode with SetHardcore
  method and frame offset logic (+14 normal/poison, +6 wither)

Also updates README changelog styling with consistent ### headings.
This commit is contained in:
itsRevela
2026-03-30 13:50:29 -05:00
parent a4f55dde16
commit 3aa2d23fa9
16 changed files with 1537 additions and 29 deletions
+115
View File
@@ -0,0 +1,115 @@
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.tags.*;
import com.jpexs.decompiler.flash.tags.base.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
/**
* Adds hardcore heart bitmap assets to a skinGraphicsHud SWF.
* These bitmaps are referenced by class name from PlaceObject3 tags in
* the health sprite frames defined in the parent skinHud/skinHDHud SWF.
*
* Usage: AddHardcoreBitmaps <skinGraphicsHud.swf> <textures-dir> <output.swf>
*/
public class AddHardcoreBitmaps {
static final String[] TEXTURE_NAMES = {
"Health_Background_Hardcore",
"Health_Background_Hardcore_Flash",
"Health_Full_Hardcore",
"Health_Half_Hardcore",
"Health_Full_Flash_Hardcore",
"Health_Half_Flash_Hardcore",
"Health_Full_Poison_Hardcore",
"Health_Half_Poison_Hardcore",
"Health_Full_Poison_Flash_Hardcore",
"Health_Half_Poison_Flash_Hardcore"
};
public static void main(String[] args) throws Exception {
if (args.length < 3) {
System.out.println("Usage: AddHardcoreBitmaps <skinGraphicsHud.swf> <textures-dir> <output.swf>");
System.exit(1);
}
String swfPath = args[0];
String texturesDir = args[1];
String outputPath = args[2];
System.out.println("Loading SWF: " + swfPath);
SWF swf = new SWF(new FileInputStream(swfPath), false);
// Find max character ID
int maxCharId = 0;
for (Tag tag : swf.getTags()) {
if (tag instanceof CharacterTag) {
int id = ((CharacterTag) tag).getCharacterId();
if (id > maxCharId) maxCharId = id;
}
}
System.out.println("Max existing character ID: " + maxCharId);
// Find SymbolClass tag
SymbolClassTag symbolClass = null;
int symbolClassIdx = -1;
for (int i = 0; i < swf.getTags().size(); i++) {
if (swf.getTags().get(i) instanceof SymbolClassTag) {
symbolClass = (SymbolClassTag) swf.getTags().get(i);
symbolClassIdx = i;
break;
}
}
if (symbolClass == null) {
System.err.println("ERROR: No SymbolClass tag found");
System.exit(1);
}
int nextCharId = maxCharId + 1;
int added = 0;
for (String texName : TEXTURE_NAMES) {
String pngFile = texturesDir + "/" + texName + ".png";
File f = new File(pngFile);
if (!f.exists()) {
System.err.println("ERROR: Missing texture: " + pngFile);
System.exit(1);
}
BufferedImage img = ImageIO.read(f);
System.out.println(" Adding " + texName + " (id=" + nextCharId + ", " + img.getWidth() + "x" + img.getHeight() + ")");
// Create DefineBitsLossless2 tag
DefineBitsLossless2Tag bmp = new DefineBitsLossless2Tag(swf);
bmp.characterID = nextCharId;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(img, "PNG", baos);
bmp.setImage(baos.toByteArray());
// Insert before SymbolClass
swf.addTag(symbolClassIdx, bmp);
symbolClassIdx++; // SymbolClass shifted by 1
// Add to SymbolClass
symbolClass.tags.add(nextCharId);
symbolClass.names.add(texName);
nextCharId++;
added++;
}
System.out.println("Added " + added + " hardcore heart bitmaps");
// Mark SymbolClass as modified so new entries get serialized
symbolClass.setModified(true);
// Save
System.out.println("Saving to: " + outputPath);
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
swf.saveTo(fos);
}
System.out.println("Done!");
}
}
+405
View File
@@ -0,0 +1,405 @@
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.tags.*;
import com.jpexs.decompiler.flash.tags.base.*;
import com.jpexs.decompiler.flash.types.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
/**
* Adds hardcore heart frames to the health sprite in skinHDHud.swf.
*
* The health sprite (FJ_Health) has 14 frames for normal/poison/wither states.
* This tool adds 10 new frames (15-24) for hardcore variants:
* Frame 15: Health_Empty_Hardcore (hardcore background)
* Frame 16: Health_Empty_Flash_Hardcore (hardcore background flash)
* Frame 17: Health_Full_Hardcore (hardcore bg + full)
* Frame 18: Health_Half_Hardcore (hardcore bg + half)
* Frame 19: Health_Full_Flash_Hardcore
* Frame 20: Health_Half_Flash_Hardcore
* Frame 21: Health_Full_Poison_Hardcore (hardcore bg + poison full)
* Frame 22: Health_Half_Poison_Hardcore
* Frame 23: Health_Full_Poison_Flash_Hardcore
* Frame 24: Health_Half_Poison_Flash_Hardcore
*
* Usage: AddHardcoreHearts <skinHDHud.swf> <textures-dir> <output.swf>
*
* The textures-dir should contain the hardcore PNGs:
* Health_Background_Hardcore.png, Health_Background_Hardcore_Flash.png,
* Health_Full_Hardcore.png, Health_Half_Hardcore.png,
* Health_Full_Flash_Hardcore.png, Health_Half_Flash_Hardcore.png,
* Health_Full_Poison_Hardcore.png, Health_Half_Poison_Hardcore.png,
* Health_Full_Poison_Flash_Hardcore.png, Health_Half_Poison_Flash_Hardcore.png
*/
public class AddHardcoreHearts {
public static void main(String[] args) throws Exception {
if (args.length < 3) {
System.out.println("Usage: AddHardcoreHearts <skinHDHud.swf> <textures-dir> <output.swf>");
System.exit(1);
}
String swfPath = args[0];
String texturesDir = args[1];
String outputPath = args[2];
System.out.println("Loading SWF: " + swfPath);
SWF swf = new SWF(new FileInputStream(swfPath), false);
// Find the FJ_Health sprite by its SymbolClass name
Map<Integer, String> symbolNames = new HashMap<>();
for (Tag tag : swf.getTags()) {
if (tag instanceof SymbolClassTag) {
SymbolClassTag sym = (SymbolClassTag) tag;
for (int i = 0; i < sym.tags.size(); i++) {
symbolNames.put(sym.tags.get(i), sym.names.get(i));
}
}
}
// Find the health sprite
DefineSpriteTag healthSprite = null;
MATRIX existingMatrix = null; // Will capture from existing PlaceObject3
for (Tag tag : swf.getTags()) {
if (tag instanceof DefineSpriteTag) {
DefineSpriteTag sprite = (DefineSpriteTag) tag;
String sym = symbolNames.getOrDefault(sprite.spriteId, "");
if (sym.contains("FJ_Health") && !sym.contains("Absorb") && !sym.contains("Horse")) {
healthSprite = sprite;
System.out.println("Found health sprite: id=" + sprite.spriteId + " sym=" + sym + " frames=" + sprite.frameCount);
// Grab the matrix from the first PlaceObject3 in the sprite (Health_Background)
for (Tag sub : sprite.getTags()) {
if (sub instanceof PlaceObject3Tag) {
PlaceObject3Tag po = (PlaceObject3Tag) sub;
if (po.matrix != null) {
existingMatrix = po.matrix;
System.out.println("Captured matrix: " + existingMatrix);
break;
}
}
}
break;
}
}
}
if (healthSprite == null) {
System.err.println("ERROR: Could not find FJ_Health sprite!");
System.exit(1);
}
// Find the highest character ID in the SWF to avoid conflicts
int maxCharId = 0;
for (Tag tag : swf.getTags()) {
if (tag instanceof CharacterTag) {
int id = ((CharacterTag) tag).getCharacterId();
if (id > maxCharId) maxCharId = id;
}
}
System.out.println("Max existing character ID: " + maxCharId);
// Load hardcore textures and create DefineBitsLossless2 tags
// We need to add these as top-level tags in the SWF, then reference them
// via PlaceObject in the health sprite frames
// Map: texture name -> charId for the new bitmap
Map<String, Integer> textureIds = new LinkedHashMap<>();
String[] textureNames = {
"Health_Background_Hardcore",
"Health_Background_Hardcore_Flash",
"Health_Full_Hardcore",
"Health_Half_Hardcore",
"Health_Full_Flash_Hardcore",
"Health_Half_Flash_Hardcore",
"Health_Full_Poison_Hardcore",
"Health_Half_Poison_Hardcore",
"Health_Full_Poison_Flash_Hardcore",
"Health_Half_Poison_Flash_Hardcore"
};
// Find where to insert new tags (before SymbolClassTag)
int insertIndex = -1;
for (int i = 0; i < swf.getTags().size(); i++) {
if (swf.getTags().get(i) instanceof SymbolClassTag) {
insertIndex = i;
break;
}
}
int nextCharId = maxCharId + 1;
SymbolClassTag symbolClass = null;
for (Tag tag : swf.getTags()) {
if (tag instanceof SymbolClassTag) {
symbolClass = (SymbolClassTag) tag;
break;
}
}
for (String texName : textureNames) {
String pngFile = texturesDir + "/" + texName + ".png";
File f = new File(pngFile);
if (!f.exists()) {
System.err.println("ERROR: Missing texture: " + pngFile);
System.exit(1);
}
BufferedImage img = ImageIO.read(f);
System.out.println(" Loading " + texName + " (" + img.getWidth() + "x" + img.getHeight() + ")");
// Create DefineBitsLossless2 tag
DefineBitsLossless2Tag bmp = new DefineBitsLossless2Tag(swf);
bmp.characterID = nextCharId;
// Convert image to PNG bytes for setImage
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(img, "PNG", baos);
bmp.setImage(baos.toByteArray());
textureIds.put(texName, nextCharId);
// Insert before SymbolClass
swf.addTag(insertIndex, bmp);
insertIndex++;
// Add to SymbolClass so it can be referenced by name
symbolClass.tags.add(nextCharId);
symbolClass.names.add(texName);
nextCharId++;
}
System.out.println("Added " + textureIds.size() + " hardcore heart bitmaps");
// Mark SymbolClass as modified so new entries get serialized
symbolClass.setModified(true);
// Now add 10 new frames to the health sprite
// Current structure: 14 frames, each has RemoveObject2, PlaceObject3(bg), PlaceObject3(heart), ShowFrame
// We follow the same pattern
// Frame structure for each hardcore frame:
// 1. FrameLabelTag (name)
// 2. RemoveObject2 depth 1 (remove old bg) - only if not first frame
// 3. PlaceObject3 cls=<bg> depth 1
// 4. RemoveObject2 depth 2 (remove old heart) - only if heart present
// 5. PlaceObject3 cls=<heart> depth 2 - if not empty
// 6. ShowFrameTag
// The frame structure from the existing SWF:
// Each frame after the first uses RemoveObject2 to clear the previous frame's objects
// then places new objects
// Define our 10 new frames
String[][] hardcoreFrames = {
// {label, bgTexture, heartTexture} (heartTexture null for empty)
{"Health_Empty_Hardcore", "Health_Background_Hardcore", null},
{"Health_Empty_Flash_Hardcore", "Health_Background_Hardcore_Flash", null},
{"Health_Full_Hardcore", "Health_Background_Hardcore", "Health_Full_Hardcore"},
{"Health_Half_Hardcore", null, "Health_Half_Hardcore"}, // bg continues from prev
{"Health_Full_Flash_Hardcore", "Health_Background_Hardcore_Flash", "Health_Full_Flash_Hardcore"},
{"Health_Half_Flash_Hardcore", null, "Health_Half_Flash_Hardcore"},
{"Health_Full_Poison_Hardcore", "Health_Background_Hardcore", "Health_Full_Poison_Hardcore"},
{"Health_Half_Poison_Hardcore", null, "Health_Half_Poison_Hardcore"},
{"Health_Full_Poison_Flash_Hardcore","Health_Background_Hardcore_Flash", "Health_Full_Poison_Flash_Hardcore"},
{"Health_Half_Poison_Flash_Hardcore",null, "Health_Half_Poison_Flash_Hardcore"},
};
// Actually, looking at the existing structure more carefully:
// Frame 1 (Health_Empty): PlaceObject3 bg@1, ShowFrame
// Frame 2 (Health_Empty_Flash): Remove@1, PlaceObject3 bg_flash@1, ShowFrame
// Frame 3 (Health_Full): Remove@1, PlaceObject3 bg@1, PlaceObject3 full@2, ShowFrame
// Frame 4 (Health_Half): PlaceObject3 half@2 (replaces heart, bg continues), ShowFrame
// Wait - frame 4 reuses bg from frame 3. Let me re-check...
// Looking at the dump:
// [8] FrameLabel "Health_Full"
// [9] PlaceObject3 cls=Health_Background dpt=1
// [10] PlaceObject3 cls=Health_Full dpt=2
// [11] ShowFrame
// [12] RemoveObject2 dpt=2
// [13] FrameLabel "Health_Half"
// [14] PlaceObject3 cls=Health_Half dpt=2
// [15] ShowFrame (bg from frame 3 still on depth 1)
// [16] RemoveObject2 dpt=1
// [17] RemoveObject2 dpt=2
// [18] FrameLabel "Health_Full_Flash"
// [19] PlaceObject3 cls=Health_Background_Flash dpt=1
// [20] PlaceObject3 cls=Health_Full_Flash dpt=2
// [21] ShowFrame
// So the pattern is:
// - When bg changes: remove depth 1, place new bg at depth 1
// - When heart changes: remove depth 2, place new heart at depth 2
// - Half frames reuse the bg from the previous Full frame
// For the last existing frame (14, Health_Half_Wither_Flash), it has objects at depth 1 and 2.
// We need to start our new frames by removing those.
// Let me follow the exact same pattern as the existing frames, starting from a clean slate
// after the last existing frame.
if (existingMatrix == null) {
System.err.println("ERROR: Could not find matrix from existing health sprite!");
System.exit(1);
}
// Build the new tags to append
List<Tag> newTags = new ArrayList<>();
// Frame structure follows the original pattern:
// Empty: remove@1, place bg@1 (depth 2 empty)
// EmptyFlash: remove@1, place flashbg@1 (depth 2 empty)
// Full: remove@1, place bg@1, place full@2 (ADD heart, no remove@2)
// Half: remove@2, place half@2 (REPLACE heart, bg continues)
// FullFlash: remove@1, place flashbg@1, remove@2, place fullflash@2
// HalfFlash: remove@2, place halfflash@2 (bg continues)
// (Poison/Wither follow the same Full/Half/FullFlash/HalfFlash pattern)
// label, rmBg, bgTex, rmHrt, hrtTex
// Frame 15: Empty - remove both from frame 14
addFrame(newTags, swf, healthSprite, "Health_Empty_Hardcore",
true, "Health_Background_Hardcore",
true, null, existingMatrix);
// Frame 16: EmptyFlash
addFrame(newTags, swf, healthSprite, "Health_Empty_Flash_Hardcore",
true, "Health_Background_Hardcore_Flash",
false, null, existingMatrix);
// Frame 17: Full - bg changes, heart ADDED (no remove@2, depth 2 was empty)
addFrame(newTags, swf, healthSprite, "Health_Full_Hardcore",
true, "Health_Background_Hardcore",
false, "Health_Full_Hardcore", existingMatrix);
// Frame 18: Half - heart REPLACED (remove@2, then place), bg continues
addFrame(newTags, swf, healthSprite, "Health_Half_Hardcore",
false, null,
true, "Health_Half_Hardcore", existingMatrix);
// Frame 19: FullFlash - both change
addFrame(newTags, swf, healthSprite, "Health_Full_Flash_Hardcore",
true, "Health_Background_Hardcore_Flash",
true, "Health_Full_Flash_Hardcore", existingMatrix);
// Frame 20: HalfFlash - heart replaced, bg continues
addFrame(newTags, swf, healthSprite, "Health_Half_Flash_Hardcore",
false, null,
true, "Health_Half_Flash_Hardcore", existingMatrix);
// Frame 21: FullPoison - both change
addFrame(newTags, swf, healthSprite, "Health_Full_Poison_Hardcore",
true, "Health_Background_Hardcore",
true, "Health_Full_Poison_Hardcore", existingMatrix);
// Frame 22: HalfPoison - heart replaced, bg continues
addFrame(newTags, swf, healthSprite, "Health_Half_Poison_Hardcore",
false, null,
true, "Health_Half_Poison_Hardcore", existingMatrix);
// Frame 23: FullPoisonFlash - both change
addFrame(newTags, swf, healthSprite, "Health_Full_Poison_Flash_Hardcore",
true, "Health_Background_Hardcore_Flash",
true, "Health_Full_Poison_Flash_Hardcore", existingMatrix);
// Frame 24: HalfPoisonFlash - heart replaced, bg continues
addFrame(newTags, swf, healthSprite, "Health_Half_Poison_Flash_Hardcore",
false, null,
true, "Health_Half_Poison_Flash_Hardcore", existingMatrix);
// Append new tags to sprite using addTag
int spriteInsertIdx = healthSprite.getTags().size();
for (Tag newTag : newTags) {
healthSprite.addTag(spriteInsertIdx, newTag);
spriteInsertIdx++;
}
healthSprite.frameCount += 10;
System.out.println("Added 10 hardcore frames. New frame count: " + healthSprite.frameCount);
// Save
System.out.println("Saving modified SWF: " + outputPath);
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
swf.saveTo(fos);
}
System.out.println("Done!");
}
/**
* Adds tags for a single frame in the health sprite.
* Uses the existingMatrix (cloned from existing PlaceObject3) to ensure correct encoding.
*/
/**
* @param removeBg emit RemoveObject2 for depth 1 before placing bg
* @param bgTexture if non-null, place this at depth 1
* @param removeHeart emit RemoveObject2 for depth 2 before placing heart
* @param heartTexture if non-null, place this at depth 2
*/
static void addFrame(List<Tag> tags, SWF swf, DefineSpriteTag parent,
String label,
boolean removeBg, String bgTexture,
boolean removeHeart, String heartTexture,
MATRIX existingMatrix) {
// Frame label
FrameLabelTag frameLabel = new FrameLabelTag(swf);
frameLabel.name = label;
frameLabel.setTimelined(parent);
tags.add(frameLabel);
// Background at depth 1
if (removeBg) {
RemoveObject2Tag rem = new RemoveObject2Tag(swf);
rem.depth = 1;
rem.setTimelined(parent);
tags.add(rem);
}
if (bgTexture != null) {
PlaceObject3Tag placeBg = new PlaceObject3Tag(swf);
placeBg.depth = 1;
placeBg.className = bgTexture;
placeBg.placeFlagHasMatrix = true;
placeBg.matrix = cloneMatrix(existingMatrix);
placeBg.placeFlagHasClassName = true;
placeBg.placeFlagHasCharacter = false;
placeBg.setTimelined(parent);
tags.add(placeBg);
}
// Heart at depth 2
if (removeHeart) {
RemoveObject2Tag rem = new RemoveObject2Tag(swf);
rem.depth = 2;
rem.setTimelined(parent);
tags.add(rem);
}
if (heartTexture != null) {
PlaceObject3Tag placeHeart = new PlaceObject3Tag(swf);
placeHeart.depth = 2;
placeHeart.className = heartTexture;
placeHeart.placeFlagHasMatrix = true;
placeHeart.matrix = cloneMatrix(existingMatrix);
placeHeart.placeFlagHasClassName = true;
placeHeart.placeFlagHasCharacter = false;
placeHeart.setTimelined(parent);
tags.add(placeHeart);
}
// ShowFrame
ShowFrameTag showFrame = new ShowFrameTag(swf);
showFrame.setTimelined(parent);
tags.add(showFrame);
}
static MATRIX cloneMatrix(MATRIX src) {
MATRIX m = new MATRIX();
m.hasScale = src.hasScale;
m.scaleX = src.scaleX;
m.scaleY = src.scaleY;
m.hasRotate = src.hasRotate;
m.rotateSkew0 = src.rotateSkew0;
m.rotateSkew1 = src.rotateSkew1;
m.translateX = src.translateX;
m.translateY = src.translateY;
return m;
}
}
+106
View File
@@ -0,0 +1,106 @@
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.tags.*;
import com.jpexs.decompiler.flash.abc.*;
import com.jpexs.decompiler.flash.abc.avm2.AVM2ConstantPool;
import com.jpexs.decompiler.flash.abc.types.*;
import com.jpexs.decompiler.flash.abc.types.traits.*;
import java.io.*;
import java.util.*;
/**
* Decompiles ActionScript 3 class/method signatures from a SWF.
* Usage: DecompileAS <swf-file>
*/
public class DecompileAS {
public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.out.println("Usage: DecompileAS <swf-file>");
return;
}
SWF swf = new SWF(new FileInputStream(args[0]), false);
for (Tag tag : swf.getTags()) {
if (tag instanceof ImportAssets2Tag) {
ImportAssets2Tag imp = (ImportAssets2Tag) tag;
System.out.println("=== ImportAssets2 ===");
System.out.println(" URL: " + imp.url);
}
if (tag instanceof DoABC2Tag) {
DoABC2Tag abcTag = (DoABC2Tag) tag;
ABC abc = abcTag.getABC();
AVM2ConstantPool cp = abc.constants;
System.out.println("=== ABC: " + abcTag.name + " ===");
System.out.println("Classes: " + abc.instance_info.size());
System.out.println();
for (int c = 0; c < abc.instance_info.size(); c++) {
InstanceInfo ii = abc.instance_info.get(c);
System.out.println("--- Class: " + resolveName(cp, ii.name_index) + " ---");
if (ii.super_index > 0)
System.out.println(" extends " + resolveName(cp, ii.super_index));
for (Trait t : ii.instance_traits.traits) {
dumpTrait(abc, cp, t, " ");
}
ClassInfo ci = abc.class_info.get(c);
for (Trait t : ci.static_traits.traits) {
System.out.print(" [static] ");
dumpTrait(abc, cp, t, "");
}
System.out.println();
}
}
}
}
static String resolveName(AVM2ConstantPool cp, int multinameIdx) {
if (multinameIdx <= 0) return "*";
Multiname mn = cp.getMultiname(multinameIdx);
String name = mn.name_index > 0 ? cp.getString(mn.name_index) : "?";
int nsIdx = mn.namespace_index;
if (nsIdx > 0) {
Namespace ns = cp.getNamespace(nsIdx);
String nsName = ns.name_index > 0 ? cp.getString(ns.name_index) : "";
if (!nsName.isEmpty()) return nsName + "." + name;
}
return name;
}
static void dumpTrait(ABC abc, AVM2ConstantPool cp, Trait t, String indent) {
try {
String name = resolveName(cp, t.name_index);
if (t instanceof TraitMethodGetterSetter) {
TraitMethodGetterSetter tm = (TraitMethodGetterSetter) t;
MethodInfo mi = abc.method_info.get(tm.method_info);
String kind = tm.kindType == Trait.TRAIT_GETTER ? "get" :
tm.kindType == Trait.TRAIT_SETTER ? "set" : "function";
StringBuilder sig = new StringBuilder();
sig.append(kind).append(" ").append(name).append("(");
for (int p = 0; p < mi.param_types.length; p++) {
if (p > 0) sig.append(", ");
sig.append(resolveName(cp, mi.param_types[p]));
}
sig.append(")");
sig.append(" : ").append(resolveName(cp, mi.ret_type));
System.out.println(indent + sig);
} else if (t instanceof TraitSlotConst) {
TraitSlotConst ts = (TraitSlotConst) t;
String type = resolveName(cp, ts.type_index);
String constKind = ts.kindType == Trait.TRAIT_CONST ? "const" : "var";
System.out.println(indent + constKind + " " + name + " : " + type);
} else if (t instanceof TraitClass) {
System.out.println(indent + "[class] " + name);
}
} catch (Exception e) {
System.out.println(indent + "[error: " + e.getMessage() + "]");
}
}
}
+48
View File
@@ -0,0 +1,48 @@
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode;
import com.jpexs.decompiler.flash.exporters.settings.ScriptExportSettings;
import com.jpexs.decompiler.flash.configuration.Configuration;
import java.io.*;
import java.util.*;
/**
* Exports all ActionScript 3 source from a SWF to a directory.
* Usage: DecompileASBody <swf-file> [output-dir]
*/
public class DecompileASBody {
public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.out.println("Usage: DecompileASBody <swf-file> [output-dir]");
return;
}
Configuration.autoDeobfuscate.set(false);
String path = args[0];
String outDir = args.length > 1 ? args[1] : "as_output";
SWF swf = new SWF(new FileInputStream(path), false);
File out = new File(outDir);
out.mkdirs();
ScriptExportSettings settings = new ScriptExportSettings(ScriptExportMode.AS, false, false, false, false);
swf.exportActionScript(null, outDir, settings, false, null);
System.out.println("Export complete. Files:");
listFiles(new File(outDir), "");
}
static void listFiles(File dir, String prefix) {
File[] files = dir.listFiles();
if (files == null) return;
Arrays.sort(files);
for (File f : files) {
if (f.isDirectory()) {
listFiles(f, prefix + f.getName() + "/");
} else {
System.out.println(" " + prefix + f.getName() + " (" + f.length() + " bytes)");
}
}
}
}
+140
View File
@@ -0,0 +1,140 @@
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.abc.*;
import com.jpexs.decompiler.flash.abc.avm2.AVM2ConstantPool;
import com.jpexs.decompiler.flash.abc.avm2.AVM2Code;
import com.jpexs.decompiler.flash.abc.avm2.instructions.*;
import com.jpexs.decompiler.flash.abc.types.*;
import com.jpexs.decompiler.flash.abc.types.traits.*;
import com.jpexs.decompiler.flash.tags.*;
import com.jpexs.decompiler.flash.configuration.Configuration;
import java.io.*;
import java.util.*;
/**
* Dumps the AVM2 bytecode of the SetHealth method from a HUD SWF.
*/
public class DumpSetHealthBC {
public static void main(String[] args) throws Exception {
Configuration.autoDeobfuscate.set(false);
SWF swf = new SWF(new FileInputStream(args[0]), false);
for (Tag tag : swf.getTags()) {
if (!(tag instanceof DoABC2Tag)) continue;
DoABC2Tag abcTag = (DoABC2Tag) tag;
ABC abc = abcTag.getABC();
AVM2ConstantPool cp = abc.constants;
// Find Hud class
for (int c = 0; c < abc.instance_info.size(); c++) {
Multiname mn = cp.getMultiname(abc.instance_info.get(c).name_index);
String name = mn.name_index > 0 ? cp.getString(mn.name_index) : "";
if (!"Hud".equals(name)) continue;
InstanceInfo ii = abc.instance_info.get(c);
// Find SetHealth method
for (Trait t : ii.instance_traits.traits) {
if (!(t instanceof TraitMethodGetterSetter)) continue;
Multiname tMn = cp.getMultiname(t.name_index);
String tName = tMn.name_index > 0 ? cp.getString(tMn.name_index) : "";
if (!"SetHealth".equals(tName)) continue;
TraitMethodGetterSetter tm = (TraitMethodGetterSetter) t;
MethodBody body = abc.findBody(tm.method_info);
if (body == null) continue;
AVM2Code code = body.getCode();
System.out.println("=== SetHealth bytecode ===");
System.out.println("max_stack=" + body.max_stack + " max_regs=" + body.max_regs);
System.out.println();
for (int i = 0; i < code.code.size(); i++) {
AVM2Instruction inst = code.code.get(i);
StringBuilder sb = new StringBuilder();
sb.append(String.format("[%3d] offset=%d opcode=0x%02X", i, inst.getAddress(), inst.definition.instructionCode));
// Show operands
if (inst.operands != null) {
for (int op : inst.operands) {
sb.append(" ").append(op);
}
}
// Try to resolve multiname operands for property instructions
if (inst.operands != null && inst.operands.length > 0) {
int mnIdx = inst.operands[0];
int opcode = inst.definition.instructionCode;
// Common property opcodes that use multiname as first operand
if (opcode == 0x66 || opcode == 0x61 || opcode == 0x5D || opcode == 0x60 ||
opcode == 0x46 || opcode == 0x4F || opcode == 0x68) {
// getproperty, setproperty, findpropstrict, getlex, callproperty, callpropvoid, initproperty
try {
Multiname propMn = cp.getMultiname(mnIdx);
String propName = propMn.name_index > 0 ? cp.getString(propMn.name_index) : "?";
sb.append(" -> ").append(propName);
} catch (Exception e) {}
}
}
// Name the opcode
String opName = getOpName(inst.definition.instructionCode);
sb.append(" (").append(opName).append(")");
System.out.println(sb);
}
}
}
}
}
static String getOpName(int opcode) {
switch (opcode) {
case 0xD0: return "getlocal0";
case 0xD1: return "getlocal1";
case 0xD2: return "getlocal2";
case 0xD3: return "getlocal3";
case 0xD4: return "setlocal0";
case 0xD5: return "setlocal1";
case 0xD6: return "setlocal2";
case 0xD7: return "setlocal3";
case 0x62: return "getlocal";
case 0x63: return "setlocal";
case 0x30: return "pushscope";
case 0x47: return "returnvoid";
case 0x48: return "returnvalue";
case 0x66: return "getproperty";
case 0x61: return "setproperty";
case 0x5D: return "findpropstrict";
case 0x60: return "getlex";
case 0x24: return "pushbyte";
case 0x25: return "pushint";
case 0x26: return "pushtrue";
case 0x27: return "pushfalse";
case 0x20: return "pushnull";
case 0xA0: return "add";
case 0x73: return "convert_i";
case 0x10: return "jump";
case 0x12: return "iffalse";
case 0x11: return "iftrue";
case 0x15: return "iflt";
case 0x16: return "ifle";
case 0x13: return "ifgt";
case 0x0C: return "ifnlt";
case 0x46: return "callproperty";
case 0x4F: return "callpropvoid";
case 0x68: return "initproperty";
case 0x4A: return "constructprop";
case 0x75: return "convert_d";
case 0x29: return "pop";
case 0x2A: return "dup";
case 0xAB: return "equals";
case 0xAD: return "lessthan";
case 0xAE: return "lessequals";
case 0xAF: return "greaterthan";
case 0xB0: return "greaterequals";
case 0x96: return "not";
case 0xA8: return "coerce_a";
default: return String.format("0x%02X", opcode);
}
}
}
+617
View File
@@ -0,0 +1,617 @@
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.abc.*;
import com.jpexs.decompiler.flash.abc.avm2.AVM2ConstantPool;
import com.jpexs.decompiler.flash.abc.types.*;
import com.jpexs.decompiler.flash.abc.types.traits.*;
import com.jpexs.decompiler.flash.tags.*;
import com.jpexs.decompiler.flash.configuration.Configuration;
import java.io.*;
import java.util.*;
/**
* Patches the HUD SWF ABC bytecode directly to add hardcore hearts support.
*
* This uses raw byte patching with manual jump offset fixups, avoiding the
* AVM2Code instruction list approach which fails to adjust existing jump
* offsets when bytes are inserted.
*
* Changes:
* 1. Adds m_bHardcore:Boolean instance variable to Hud class
* 2. Adds SetHardcore(Boolean) method
* 3. Modifies SetHealth to check m_bHardcore and offset heart frames
*
* Usage: PatchHudABC <hud.swf> <output.swf>
*/
public class PatchHudABC {
// AVM2 opcodes that have s24 jump offset as first operand
static final Set<Integer> JUMP_OPCODES = Set.of(
0x10, // jump
0x11, // iftrue
0x12, // iffalse
0x13, // ifeq
0x14, // ifne
0x15, // iflt
0x16, // ifle
0x17, // ifgt
0x18, // ifge
0x19, // ifstricteq
0x1A, // ifstrictne
0x0C, // ifnlt
0x0D, // ifnle
0x0E, // ifngt
0x0F // ifnge
);
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("Usage: PatchHudABC <hud.swf> <output.swf>");
return;
}
Configuration.autoDeobfuscate.set(false);
String inputPath = args[0];
String outputPath = args[1];
System.out.println("Loading SWF: " + inputPath);
SWF swf = new SWF(new FileInputStream(inputPath), false);
for (Tag tag : swf.getTags()) {
if (!(tag instanceof DoABC2Tag)) continue;
DoABC2Tag abcTag = (DoABC2Tag) tag;
ABC abc = abcTag.getABC();
AVM2ConstantPool cp = abc.constants;
// Find the Hud class
int hudClassIdx = -1;
for (int c = 0; c < abc.instance_info.size(); c++) {
Multiname mn = cp.getMultiname(abc.instance_info.get(c).name_index);
String name = mn.name_index > 0 ? cp.getString(mn.name_index) : "";
if ("Hud".equals(name)) {
hudClassIdx = c;
break;
}
}
if (hudClassIdx < 0) {
System.err.println("ERROR: Could not find Hud class");
System.exit(1);
}
System.out.println("Found Hud class at index " + hudClassIdx);
InstanceInfo hudInstance = abc.instance_info.get(hudClassIdx);
// === Step 1: Add m_bHardcore Boolean member variable ===
int strHardcore = ensureString(cp, "m_bHardcore");
int mnBoolean = findMultinameByName(cp, "Boolean");
if (mnBoolean < 0) {
System.err.println("ERROR: Could not find Boolean multiname");
System.exit(1);
}
// Find the private namespace for the Hud class (same as m_bWithered uses)
int privateNs = -1;
for (Trait t : hudInstance.instance_traits.traits) {
Multiname tMn = cp.getMultiname(t.name_index);
String tName = tMn.name_index > 0 ? cp.getString(tMn.name_index) : "";
if ("m_bWithered".equals(tName)) {
privateNs = tMn.namespace_index;
break;
}
}
if (privateNs < 0) {
System.err.println("ERROR: Could not find m_bWithered private namespace");
System.exit(1);
}
// Create QName multiname for m_bHardcore
int mnHardcore = addQName(cp, strHardcore, privateNs);
// Add trait
TraitSlotConst hardcoreSlot = new TraitSlotConst();
hardcoreSlot.name_index = mnHardcore;
hardcoreSlot.kindType = Trait.TRAIT_SLOT;
hardcoreSlot.type_index = mnBoolean;
hardcoreSlot.slot_id = 0; // auto-assign
hudInstance.instance_traits.traits.add(hardcoreSlot);
System.out.println("Added m_bHardcore member variable");
// === Step 2: Add SetHardcore method ===
int strSetHardcore = ensureString(cp, "SetHardcore");
// Find public namespace (same as SetHealth uses)
int publicNs = -1;
for (Trait t : hudInstance.instance_traits.traits) {
if (t instanceof TraitMethodGetterSetter) {
Multiname tMn = cp.getMultiname(t.name_index);
String tName = tMn.name_index > 0 ? cp.getString(tMn.name_index) : "";
if ("SetHealth".equals(tName)) {
publicNs = tMn.namespace_index;
break;
}
}
}
if (publicNs < 0) {
System.err.println("ERROR: Could not find SetHealth public namespace");
System.exit(1);
}
int mnSetHardcore = addQName(cp, strSetHardcore, publicNs);
// Create method info for SetHardcore(Boolean):void
int retTypeVoid = findMultinameByName(cp, "void");
if (retTypeVoid < 0) retTypeVoid = 0;
MethodInfo setHardcoreMethod = new MethodInfo(
new int[]{mnBoolean}, retTypeVoid, 0, 0, null, null
);
abc.method_info.add(setHardcoreMethod);
int setHardcoreMethodIdx = abc.method_info.size() - 1;
// Find an existing instance method body to copy scope depth from
int refInitScope = 10;
int refMaxScope = 11;
for (Trait t : hudInstance.instance_traits.traits) {
if (t instanceof TraitMethodGetterSetter) {
MethodBody refBody = abc.findBody(((TraitMethodGetterSetter) t).method_info);
if (refBody != null) {
refInitScope = refBody.init_scope_depth;
refMaxScope = refBody.max_scope_depth;
break;
}
}
}
// Create method body with raw bytes:
// D0 getlocal0
// 30 pushscope
// D0 getlocal0
// D1 getlocal1
// 61 XX XX setproperty m_bHardcore (u30 multiname)
// 47 returnvoid
MethodBody setHardcoreBody = new MethodBody();
setHardcoreBody.method_info = setHardcoreMethodIdx;
setHardcoreBody.max_stack = 2;
setHardcoreBody.max_regs = 2;
setHardcoreBody.init_scope_depth = refInitScope;
setHardcoreBody.max_scope_depth = refMaxScope;
byte[] mnHardcoreU30 = encodeU30(mnHardcore);
byte[] shBytes = new byte[4 + 1 + mnHardcoreU30.length + 1];
int pos = 0;
shBytes[pos++] = (byte) 0xD0; // getlocal0
shBytes[pos++] = (byte) 0x30; // pushscope
shBytes[pos++] = (byte) 0xD0; // getlocal0
shBytes[pos++] = (byte) 0xD1; // getlocal1
shBytes[pos++] = (byte) 0x61; // setproperty
System.arraycopy(mnHardcoreU30, 0, shBytes, pos, mnHardcoreU30.length);
pos += mnHardcoreU30.length;
shBytes[pos++] = (byte) 0x47; // returnvoid
setHardcoreBody.setCodeBytes(shBytes);
abc.bodies.add(setHardcoreBody);
// Create trait for SetHardcore method
TraitMethodGetterSetter setHardcoreTrait = new TraitMethodGetterSetter();
setHardcoreTrait.name_index = mnSetHardcore;
setHardcoreTrait.kindType = Trait.TRAIT_METHOD;
setHardcoreTrait.method_info = setHardcoreMethodIdx;
hudInstance.instance_traits.traits.add(setHardcoreTrait);
System.out.println("Added SetHardcore method");
// === Step 3: Patch SetHealth raw bytes ===
for (Trait t : hudInstance.instance_traits.traits) {
if (!(t instanceof TraitMethodGetterSetter)) continue;
Multiname tMn = cp.getMultiname(t.name_index);
String tName = tMn.name_index > 0 ? cp.getString(tMn.name_index) : "";
if (!"SetHealth".equals(tName)) continue;
TraitMethodGetterSetter tm = (TraitMethodGetterSetter) t;
MethodBody body = abc.findBody(tm.method_info);
if (body == null) continue;
System.out.println("Patching SetHealth method body...");
patchSetHealthRawBytes(body, mnHardcore);
// Bump max_stack if needed
if (body.max_stack < 3) body.max_stack = 3;
break;
}
abcTag.setModified(true);
break;
}
System.out.println("Saving to: " + outputPath);
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
swf.saveTo(fos);
}
System.out.println("Done!");
}
/**
* Patches SetHealth by inserting raw bytes after the setlocal 7 instruction.
*
* The patch implements:
* if (m_bHardcore) {
* if (frame >= 11) frame += 6; // wither -> hardcore normal
* else frame += 14; // normal/poison -> hardcore
* }
*/
static void patchSetHealthRawBytes(MethodBody body, int mnHardcore) {
byte[] code = body.getCodeBytes();
// Find "setlocal 7" (0x63 0x07) after byte offset ~150
int insertAt = -1;
for (int i = 150; i < code.length - 1; i++) {
if ((code[i] & 0xFF) == 0x63 && (code[i + 1] & 0xFF) == 0x07) {
insertAt = i + 2; // insert AFTER setlocal 7
break;
}
}
if (insertAt < 0) {
System.err.println("ERROR: Could not find setlocal 7 in SetHealth bytecode");
System.exit(1);
}
System.out.println("Inserting patch at byte offset " + insertAt);
// Build the patch bytes manually.
// Layout with byte sizes:
//
// D0 getlocal0 (1)
// 66 XX.. getproperty m_bHardcore (u30) (1 + u30len)
// 12 YY YY YY iffalse <skip to end> (4)
// 62 07 getlocal 7 (2)
// 24 0B pushbyte 11 (2)
// 15 ZZ ZZ ZZ iflt <to add14 block> (4)
// 62 07 getlocal 7 (2)
// 24 06 pushbyte 6 (2)
// A0 add (1)
// 73 convert_i (1)
// 63 07 setlocal 7 (2)
// 10 WW WW WW jump <to end> (4)
// --- add14 block ---
// 62 07 getlocal 7 (2)
// 24 0E pushbyte 14 (2)
// A0 add (1)
// 73 convert_i (1)
// 63 07 setlocal 7 (2)
// --- end ---
byte[] mnU30 = encodeU30(mnHardcore);
int mnU30Len = mnU30.length;
// Calculate block sizes for jump offset computation
// iffalse is at offset: 1 + 1 + mnU30Len = (2 + mnU30Len) from patch start
// iffalse instruction ends at: (2 + mnU30Len) + 4 = (6 + mnU30Len)
//
// After iffalse: getlocal7(2) + pushbyte11(2) + iflt(4) = 8 bytes
// iflt is at offset: (6 + mnU30Len) + 4 = (10 + mnU30Len) from patch start
// iflt instruction ends at: (10 + mnU30Len) + 4 = (14 + mnU30Len)
//
// add6 block: getlocal7(2) + pushbyte6(2) + add(1) + convert_i(1) + setlocal7(2) + jump(4) = 12 bytes
// jump is at offset: (14 + mnU30Len) + 8 = (22 + mnU30Len) from patch start
// jump instruction ends at: (22 + mnU30Len) + 4 = (26 + mnU30Len)
//
// add14 block starts at offset: (26 + mnU30Len) from patch start
// add14 block: getlocal7(2) + pushbyte14(2) + add(1) + convert_i(1) + setlocal7(2) = 8 bytes
// Total patch size: (26 + mnU30Len) + 8 = (34 + mnU30Len)
int add6BlockStart = 6 + mnU30Len; // after iffalse instruction
int ifltEnd = 14 + mnU30Len; // after iflt instruction
int jumpEnd = 26 + mnU30Len; // after jump instruction
int add14BlockStart = 26 + mnU30Len; // start of add14 block
int patchEnd = 34 + mnU30Len; // end of entire patch
// iffalse offset: skip from end of iffalse to end of patch
// s24 = target - instructionEnd = patchEnd - (6 + mnU30Len)
int iffalseOffset = patchEnd - (6 + mnU30Len);
// iflt offset: skip from end of iflt to add14 block start
// s24 = add14BlockStart - ifltEnd
int ifltOffset = add14BlockStart - ifltEnd;
// jump offset: skip from end of jump to end of patch
// s24 = patchEnd - jumpEnd
int jumpOffset = patchEnd - jumpEnd;
// Build patch byte array
byte[] patch = new byte[patchEnd];
int p = 0;
// getlocal0
patch[p++] = (byte) 0xD0;
// getproperty m_bHardcore
patch[p++] = (byte) 0x66;
System.arraycopy(mnU30, 0, patch, p, mnU30Len);
p += mnU30Len;
// iffalse <skip to end>
patch[p++] = (byte) 0x12;
writeS24(patch, p, iffalseOffset);
p += 3;
// getlocal 7
patch[p++] = (byte) 0x62;
patch[p++] = (byte) 0x07;
// pushbyte 11
patch[p++] = (byte) 0x24;
patch[p++] = (byte) 0x0B;
// iflt <to add14 block>
patch[p++] = (byte) 0x15;
writeS24(patch, p, ifltOffset);
p += 3;
// getlocal 7
patch[p++] = (byte) 0x62;
patch[p++] = (byte) 0x07;
// pushbyte 6
patch[p++] = (byte) 0x24;
patch[p++] = (byte) 0x06;
// add
patch[p++] = (byte) 0xA0;
// convert_i
patch[p++] = (byte) 0x73;
// setlocal 7
patch[p++] = (byte) 0x63;
patch[p++] = (byte) 0x07;
// jump <to end>
patch[p++] = (byte) 0x10;
writeS24(patch, p, jumpOffset);
p += 3;
// --- add14 block ---
// getlocal 7
patch[p++] = (byte) 0x62;
patch[p++] = (byte) 0x07;
// pushbyte 14
patch[p++] = (byte) 0x24;
patch[p++] = (byte) 0x0E;
// add
patch[p++] = (byte) 0xA0;
// convert_i
patch[p++] = (byte) 0x73;
// setlocal 7
patch[p++] = (byte) 0x63;
patch[p++] = (byte) 0x07;
System.out.println("Patch is " + patch.length + " bytes, inserting at offset " + insertAt);
// Build new code with patch inserted
byte[] newCode = new byte[code.length + patch.length];
System.arraycopy(code, 0, newCode, 0, insertAt);
System.arraycopy(patch, 0, newCode, insertAt, patch.length);
System.arraycopy(code, insertAt, newCode, insertAt + patch.length, code.length - insertAt);
// Fix all existing jump offsets that cross the insertion point
fixJumpOffsets(newCode, insertAt, patch.length);
body.setCodeBytes(newCode);
System.out.println("Patched SetHealth: " + code.length + " -> " + newCode.length + " bytes");
}
// === Helper methods ===
static int ensureString(AVM2ConstantPool cp, String s) {
for (int i = 1; i < cp.getStringCount(); i++) {
if (s.equals(cp.getString(i))) return i;
}
return cp.addString(s);
}
static int findMultinameByName(AVM2ConstantPool cp, String name) {
for (int i = 1; i < cp.getMultinameCount(); i++) {
Multiname mn = cp.getMultiname(i);
if (mn.name_index > 0 && name.equals(cp.getString(mn.name_index))) {
return i;
}
}
return -1;
}
static int addQName(AVM2ConstantPool cp, int nameIndex, int nsIndex) {
for (int i = 1; i < cp.getMultinameCount(); i++) {
Multiname mn = cp.getMultiname(i);
if (mn.kind == Multiname.QNAME && mn.name_index == nameIndex && mn.namespace_index == nsIndex) {
return i;
}
}
Multiname mn = new Multiname();
mn.kind = Multiname.QNAME;
mn.name_index = nameIndex;
mn.namespace_index = nsIndex;
return cp.addMultiname(mn);
}
/** Encode an integer as AVM2 u30 (variable-length unsigned 30-bit integer). */
static byte[] encodeU30(int val) {
ByteArrayOutputStream buf = new ByteArrayOutputStream(5);
do {
int b = val & 0x7F;
val >>>= 7;
if (val != 0) b |= 0x80;
buf.write(b);
} while (val != 0);
return buf.toByteArray();
}
// === Jump offset fixup methods (from TestIsolate.java) ===
/**
* Scans the bytecode for jump instructions and adjusts their s24 offsets
* to account for insertSize bytes inserted at insertPos.
*/
static void fixJumpOffsets(byte[] code, int insertPos, int insertSize) {
int i = 0;
while (i < code.length) {
int opcode = code[i] & 0xFF;
if (JUMP_OPCODES.contains(opcode)) {
int jumpInstrEnd = i + 4;
int offset = readS24(code, i + 1);
boolean jumpIsBeforeInsert = (i < insertPos);
boolean jumpIsAfterInsert = (i >= insertPos + insertSize);
if (jumpIsBeforeInsert) {
int origTarget = jumpInstrEnd + offset;
if (origTarget >= insertPos) {
writeS24(code, i + 1, offset + insertSize);
System.out.println(" Fixed forward jump at " + i + ": " + offset + " -> " + (offset + insertSize));
}
} else if (jumpIsAfterInsert) {
int origJumpEnd = (i - insertSize) + 4;
int origTarget = origJumpEnd + offset;
if (origTarget < insertPos) {
writeS24(code, i + 1, offset - insertSize);
System.out.println(" Fixed backward jump at " + i + ": " + offset + " -> " + (offset - insertSize));
}
}
i += 4;
} else {
i += instructionSize(code, i);
}
}
}
static int readS24(byte[] code, int pos) {
int b0 = code[pos] & 0xFF;
int b1 = code[pos + 1] & 0xFF;
int b2 = code[pos + 2] & 0xFF;
int val = b0 | (b1 << 8) | (b2 << 16);
if ((val & 0x800000) != 0) val |= 0xFF000000;
return val;
}
static void writeS24(byte[] code, int pos, int val) {
code[pos] = (byte) (val & 0xFF);
code[pos + 1] = (byte) ((val >> 8) & 0xFF);
code[pos + 2] = (byte) ((val >> 16) & 0xFF);
}
/** Returns the byte size of the instruction at the given offset. */
static int instructionSize(byte[] code, int pos) {
int op = code[pos] & 0xFF;
// No-operand instructions (1 byte)
if (op >= 0x01 && op <= 0x0B) return 1;
if (op == 0x1E || op == 0x1F) return 1;
if (op >= 0x20 && op <= 0x23) return 1;
if (op == 0x26 || op == 0x27) return 1;
if (op == 0x28 || op == 0x29 || op == 0x2A) return 1;
if (op == 0x2B) return 1;
if (op == 0x30) return 1;
if (op == 0x47 || op == 0x48) return 1;
if (op >= 0x57 && op <= 0x5A) return 1;
if (op >= 0x70 && op <= 0x78) return 1;
if (op == 0x79 || op == 0x7A) return 1;
if (op >= 0x80 && op <= 0x85) return 1;
if (op >= 0x87 && op <= 0x99) return 1;
if (op == 0x9A) return 1;
if (op == 0x9B || op == 0x9C) return 1;
if (op >= 0xA0 && op <= 0xB4) return 1;
if (op >= 0xC0 && op <= 0xC7) return 2;
if (op >= 0xD0 && op <= 0xD7) return 1;
// Opcodes with s24 operand (4 bytes total)
if (JUMP_OPCODES.contains(op)) return 4;
// Opcodes with u30 operand
if (op == 0x24) return 2; // pushbyte (u8)
if (op == 0x25) return 1 + u30Len(code, pos + 1); // pushint
if (op == 0x2C || op == 0x2D) return 1 + u30Len(code, pos + 1); // pushstring, pushint
if (op == 0x2E || op == 0x2F) return 1 + u30Len(code, pos + 1); // pushuint, pushdouble
if (op == 0x31) return 1 + u30Len(code, pos + 1); // pushnamespace
if (op == 0x32) return 5; // hasnext2
// Single u30 operand instructions
if (op == 0x40) return 1 + u30Len(code, pos + 1); // newfunction
if (op == 0x41 || op == 0x42) return 1 + u30Len(code, pos + 1); // call, construct
if (op == 0x43) return 1 + u30Len(code, pos + 1) + u30Len(code, pos + 1 + u30Len(code, pos + 1)); // callmethod
if (op == 0x44 || op == 0x45) return 1 + u30Len(code, pos + 1) + u30Len(code, pos + 1 + u30Len(code, pos + 1)); // callstatic, callsuper
if (op == 0x46) return 1 + u30Len(code, pos + 1) + u30Len(code, pos + 1 + u30Len(code, pos + 1)); // callproperty
if (op == 0x49) return 1 + u30Len(code, pos + 1); // constructsuper
if (op == 0x4A) return 1 + u30Len(code, pos + 1) + u30Len(code, pos + 1 + u30Len(code, pos + 1)); // constructprop
if (op == 0x4C) return 1 + u30Len(code, pos + 1) + u30Len(code, pos + 1 + u30Len(code, pos + 1)); // callproplex
if (op == 0x4E) return 1 + u30Len(code, pos + 1) + u30Len(code, pos + 1 + u30Len(code, pos + 1)); // callsupervoid
if (op == 0x4F) return 1 + u30Len(code, pos + 1) + u30Len(code, pos + 1 + u30Len(code, pos + 1)); // callpropvoid
// Single u30 operand
if (op == 0x53) return 1 + u30Len(code, pos + 1); // applytype
if (op == 0x55) return 1 + u30Len(code, pos + 1); // newobject
if (op == 0x56) return 1 + u30Len(code, pos + 1); // newarray
if (op == 0x58) return 1 + u30Len(code, pos + 1); // newclass
if (op == 0x59) return 1 + u30Len(code, pos + 1); // getdescendants
if (op == 0x5A) return 1 + u30Len(code, pos + 1); // newcatch
if (op == 0x5D || op == 0x5E) return 1 + u30Len(code, pos + 1); // findpropstrict, findproperty
if (op == 0x60) return 1 + u30Len(code, pos + 1); // getlex
if (op == 0x61) return 1 + u30Len(code, pos + 1); // setproperty
if (op == 0x62) return 1 + u30Len(code, pos + 1); // getlocal
if (op == 0x63) return 1 + u30Len(code, pos + 1); // setlocal
if (op == 0x65) return 1 + u30Len(code, pos + 1); // getscopeobject
if (op == 0x66) return 1 + u30Len(code, pos + 1); // getproperty
if (op == 0x68) return 1 + u30Len(code, pos + 1); // initproperty
if (op == 0x6A) return 1 + u30Len(code, pos + 1); // deleteproperty
if (op == 0x6C) return 1 + u30Len(code, pos + 1); // getslot
if (op == 0x6D) return 1 + u30Len(code, pos + 1); // setslot
if (op == 0x86) return 1 + u30Len(code, pos + 1); // astype
if (op == 0x80) return 1 + u30Len(code, pos + 1); // coerce
// debug (0xEF): special format
if (op == 0xEF) {
int off = 1;
off += 1; // debug_type (u8)
off += u30Len(code, pos + off); // index (u30)
off += 1; // reg (u8)
off += u30Len(code, pos + off); // extra (u30)
return off;
}
// lookupswitch (0x1B): s24 + u30 count + (count+1) s24 offsets
if (op == 0x1B) {
int off = 4; // opcode + s24 default
int count = readU30(code, pos + off);
off += u30Len(code, pos + off);
off += (count + 1) * 3; // s24 offsets
return off;
}
// Default: assume 1 byte
return 1;
}
static int u30Len(byte[] code, int pos) {
int len = 1;
while ((code[pos + len - 1] & 0x80) != 0 && len < 5) len++;
return len;
}
static int readU30(byte[] code, int pos) {
int result = 0;
int shift = 0;
for (int i = 0; i < 5; i++) {
int b = code[pos + i] & 0xFF;
result |= (b & 0x7F) << shift;
if ((b & 0x80) == 0) break;
shift += 7;
}
return result;
}
}