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:
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user