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 1538 additions and 30 deletions

BIN
.github/hardcore-hearts.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -5,6 +5,8 @@
#include "..\..\..\Minecraft.World\net.minecraft.world.item.h"
#include "..\..\..\Minecraft.World\net.minecraft.world.entity.ai.attributes.h"
#include "..\..\..\Minecraft.World\net.minecraft.world.entity.monster.h"
#include "..\..\MultiPlayerLevel.h"
#include "..\..\..\Minecraft.World\LevelData.h"
#include "IUIScene_HUD.h"
#include "UI.h"
@@ -20,6 +22,7 @@ IUIScene_HUD::IUIScene_HUD()
m_lastMaxHealth = 20;
m_lastHealthBlink = false;
m_lastHealthPoison = false;
m_lastHealthHardcore = false;
m_iCurrentFood = -1;
m_lastFoodPoison = false;
m_lastAir = 10;
@@ -94,9 +97,10 @@ void IUIScene_HUD::updateFrameTick()
ShowHealth(false);
ShowFood(false);
ShowAir(false);
ShowArmour(false);
ShowArmour(false);
ShowExpBar(false);
SetHealthAbsorb(0);
SetHealthAbsorb(0);
SetHardcoreMode(false);
}
if(pMinecraft->localplayers[iPad]->isRidingJumpable())
@@ -206,6 +210,12 @@ void IUIScene_HUD::renderPlayerHealth()
// Update armour
int armor = pMinecraft->localplayers[iPad]->getArmorValue();
// Check hardcore mode
bool bHardcore = pMinecraft->level != nullptr
&& pMinecraft->level->getLevelData() != nullptr
&& pMinecraft->level->getLevelData()->isHardcore();
SetHardcoreMode(bHardcore);
SetHealth(currentHealth, oldHealth, blink, bHasPoison || bHasWither, bHasWither);
SetHealthAbsorb(totalAbsorption);

View File

@@ -11,6 +11,7 @@ protected:
int m_iCurrentHealth;
int m_lastMaxHealth;
bool m_lastHealthBlink, m_lastHealthPoison, m_lastHealthWither;
bool m_lastHealthHardcore;
int m_iCurrentFood;
bool m_lastFoodPoison;
int m_lastAir, m_currentExtraAir;
@@ -46,6 +47,7 @@ protected:
virtual void SetActiveSlot(int slot) = 0;
virtual void SetHealth(int iHealth, int iLastHealth, bool bBlink, bool bPoison, bool bWither) = 0;
virtual void SetHardcoreMode(bool bHardcore) = 0;
virtual void SetFood(int iFood, int iLastFood, bool bPoison) = 0;
virtual void SetAir(int iAir, int extra) = 0;
virtual void SetArmour(int iArmour) = 0;

View File

@@ -461,6 +461,8 @@ void UIScene_CreateWorldMenu::handlePress(F64 controlId, F64 childId)
}
break;
case eControl_GameModeToggle:
if (s_bHardcore)
break; // Hardcore mode locks game mode to Survival
switch(m_iGameModeId)
{
case 0: // Creative
@@ -473,7 +475,7 @@ void UIScene_CreateWorldMenu::handlePress(F64 controlId, F64 childId)
m_iGameModeId = GameType::ADVENTURE->getId();
m_bGameModeCreative = false;
break;
case 2: // Survival
case 2: // Survival
m_buttonGamemode.setLabel(app.GetString(IDS_GAMEMODE_SURVIVAL));
m_iGameModeId = GameType::SURVIVAL->getId();
m_bGameModeCreative = false;
@@ -665,6 +667,14 @@ void UIScene_CreateWorldMenu::handleSliderMove(F64 sliderId, F64 currentValue)
else
swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[value]));
m_sliderDifficulty.setLabel(TempString);
// Hardcore locks game mode to Survival
if (s_bHardcore && m_iGameModeId != GameType::SURVIVAL->getId())
{
m_iGameModeId = GameType::SURVIVAL->getId();
m_bGameModeCreative = false;
m_buttonGamemode.setLabel(app.GetString(IDS_GAMEMODE_SURVIVAL));
}
break;
}
}

View File

@@ -655,6 +655,23 @@ void UIScene_HUD::SetHorseJumpBarProgress(float progress)
}
}
void UIScene_HUD::SetHardcoreMode(bool bHardcore)
{
IggyDataValue result;
IggyDataValue value[1];
value[0].type = IGGY_DATATYPE_boolean;
value[0].boolval = bHardcore;
IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSetHardcore , 1 , value );
// When hardcore state changes, invalidate SetHealth's dirty check
// so hearts are redrawn with the correct frame set on the next tick
if(bHardcore != m_lastHealthHardcore)
{
m_lastHealthHardcore = bHardcore;
m_lastMaxHealth = -1;
}
}
void UIScene_HUD::SetHealthAbsorb(int healthAbsorb)
{
if(m_iCurrentHealthAbsorb != healthAbsorb)

View File

@@ -25,6 +25,7 @@ protected:
IggyName m_funcRepositionHud, m_funcSetDisplayName, m_funcSetTooltipsEnabled;
IggyName m_funcSetRidingHorse, m_funcSetHorseHealth, m_funcSetHorseJumpBarProgress;
IggyName m_funcSetHealthAbsorb;
IggyName m_funcSetHardcore;
UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene)
UI_MAP_ELEMENT(m_labelChatText[0],"Label1")
UI_MAP_ELEMENT(m_labelChatText[1],"Label2")
@@ -89,6 +90,7 @@ protected:
UI_MAP_NAME(m_funcSetHorseJumpBarProgress, L"SetHorseJumpBarProgress")
UI_MAP_NAME(m_funcSetHealthAbsorb, L"SetHealthAbsorb")
UI_MAP_NAME(m_funcSetHardcore, L"SetHardcore")
UI_END_MAP_ELEMENTS_AND_NAMES()
public:
@@ -159,6 +161,8 @@ private:
void SetHealthAbsorb(int healthAbsorb);
void SetHardcoreMode(bool bHardcore);
public:
void SetSelectedLabel(const wstring &label);
void ShowDisplayName(bool show);

View File

@@ -579,6 +579,11 @@ void UIScene_LoadMenu::tick()
WCHAR TempString[256];
swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ), L"Hardcore");
m_sliderDifficulty.init(TempString, eControl_Difficulty, 0, 4, 4);
// Hardcore locks game mode to Survival
m_iGameModeId = GameType::SURVIVAL->getId();
m_bGameModeCreative = false;
m_buttonGamemode.setLabel(app.GetString(IDS_GAMEMODE_SURVIVAL));
}
}
@@ -733,6 +738,8 @@ void UIScene_LoadMenu::handlePress(F64 controlId, F64 childId)
switch(static_cast<int>(controlId))
{
case eControl_GameMode:
if (m_bHardcore)
break; // Hardcore mode locks game mode to Survival
switch(m_iGameModeId)
{
case 0: // Survival

View File

@@ -1008,8 +1008,11 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring
#endif
levels[i]->getLevelData()->setGameType(gameType);
// Apply hardcore flag from host option to level data so loaded worlds respect server.properties
#ifdef MINECRAFT_SERVER_BUILD
// Dedicated server: server.properties hardcore flag is authoritative
levels[i]->getLevelData()->setHardcore(isHardcore());
#endif
// Offline/client-hosted: keep the world's saved hardcore flag from NBT
if(app.getLevelGenerationOptions() != nullptr)
{

View File

@@ -14,6 +14,15 @@ This project is based on source code of Minecraft Legacy Console Edition v1.6.05
## Latest:
### Hardcore Hearts
![Hardcore Hearts](.github/hardcore-hearts.png)
- Worlds in hardcore mode now display the hardcore heart textures, matching Java Edition
- Supports all heart states: normal, poison, wither, and flash/blink animations
- Works across all contexts: offline worlds, online hosted worlds, and dedicated servers
- Game mode is locked to Survival when hardcore is enabled in the world creation and load screens
### Dedicated Server Security Hardening
The dedicated server now includes a comprehensive security system to protect against packet-sniffing attacks, XUID harvesting, privilege escalation, and bot flooding. All features are configurable in `server.properties`. Compatible with [playit.gg](https://playit.gg) -- enable `proxy-protocol=true` in your server.properties and enable PROXY Protocol v1 in your playit.gg tunnel settings to get per-player IP tracking, IP bans, and per-player rate limiting.
@@ -62,63 +71,72 @@ proxy-protocol=true
**Important:** When `require-secure-client=true` and `enable-stream-cipher=true`, only the secured client (`LCREWindows64.zip`) can connect. Old/upstream clients will be blocked before receiving any game data. Set both to `false` if you want to allow all clients.
---
### Player List Map Icon Color Fix
Player list map icon color fix:
- The colored map icon shown next to each player in the tab player list and teleport menu now matches their actual map marker color. Previously the icon was determined by a broken small-ID lookup that produced incorrect colors. The icon is now computed client-side using the same hash the map renderer uses, keyed by player name for reliable lookup
End dimension fixes for dedicated servers:
### End Dimension Fixes for Dedicated Servers
- Fixed the Ender Dragon being immune to melee damage on dedicated servers. The server's entity ID allocator (smallId pool) assigned non-sequential IDs to the dragon's body parts, but the client assumed sequential offsets. Melee attacks targeted IDs the server didn't recognize, so hits were silently dropped. The server now reassigns sub-entity IDs to be sequential from the parent when an entity with parts is added to the level
- Fixed entering the End exit portal after defeating the dragon crashing the game. The player entity was never added to the Overworld level during the dimension transition, leaving the player as a ghost entity that caused a crash on the next interaction
- Fixed the End Poem crashing the client on dedicated servers due to an out-of-bounds player index lookup in the WIN_GAME event handler
Dedicated server player list fix:
### Dedicated Server Player List Fix
- The Tab player list now correctly shows all connected players on dedicated servers. Previously only the local player was visible because remote players were never registered in the client's network player tracking when their `AddPlayerPacket` arrived
- The dedicated server's phantom host entry (slot 0, empty name) is now filtered from the list
- Players are properly removed from the list when they disconnect, using gamertag matching since dedicated server XUIDs are not available on the client
SRV record support and async join refactor:
### SRV Record Support and Async Join Refactor
- Added DNS SRV record resolution (`_minecraft._tcp.<hostname>`), matching Java Edition behavior. Players can connect using just a domain name (e.g. `play.example.com`) and the client will automatically look up the correct server address and port from DNS
- Refactored the async server joining system: replaced boolean flags with a clean `eJoinState` enum state machine, moved connection progress handling into a dedicated `UIScene_ConnectingProgress` class with attempt counter and cancel support, and added a `FinalizeJoin()` separation so the recv thread only starts after the UI confirms success
Piston fix for dedicated servers:
### Piston Fix for Dedicated Servers
- Fixed a bug where pistons would permanently break server-wide on dedicated servers when a redstone clock ran long enough. The piston update lock (`ignoreUpdate`) was set at the start of `triggerEvent` but never cleared on three early-return paths, permanently blocking all piston neighbor updates for the rest of the session. A fast clock would eventually hit one of these paths (e.g. signal state changing between event queuing and processing), locking out every piston in the world
Chunk unloading and connection stability fixes:
### Chunk Unloading and Connection Stability Fixes
- Fixed a regression where chunks outside the player's immediate vicinity would fail to load on dedicated servers, leaving giant missing areas. The server's chunk drop function was immediately removing chunks from the cache instead of queuing them for the existing save/unload pipeline, which meant chunks were never saved, never moved to the recovery cache, and their entities (item frames, paintings, etc.) were never removed from the level before being reloaded, causing entity duplication
- Fixed the server's `dropAll()` and autosave chunk cleanup iterating the loaded chunk list while simultaneously modifying it (undefined behavior that could corrupt chunk tracking or stall the server)
- Removed an overly aggressive `dropAll()` call that wiped the entire chunk cache whenever render distance decreased, instead of only removing the out-of-range chunks
- Fixed a client-side connection bug where a 5-second socket recv timeout (used during the initial server handshake) was never cleared after connecting. This meant any brief server pause longer than 5 seconds (e.g. autosave, chunk I/O) would cause the client to interpret the silence as a lost connection and disconnect
Dedicated server biome diversity fix:
### Dedicated Server Biome Diversity Fix
- The dedicated server previously used a completely random seed with no biome diversity checks, unlike the client which validates seeds to guarantee varied biomes. This could result in server worlds with large regions dominated by only one or two biome types (e.g. all taiga/snowy)
- On top of that, the client's seed validation was hardcoded to only check a 54-chunk (Classic) area, so even validated seeds had no diversity guarantee beyond that. This made the problem especially noticeable on Large worlds or worlds expanded from Classic to Large
- New server worlds now validate seeds for biome diversity, and the validation scales to the full target world size
- Added `override-seed` in server.properties to fix existing worlds without deleting them. Set it to any seed number and newly generated chunks will use it instead of the original
Server list and connection improvements:
### Server List and Connection Improvements
- Server edits and deletions now apply immediately without needing to restart the game
- Connecting to an offline/unreachable server no longer freezes the game indefinitely
- Connection attempts use non-blocking sockets with a 5-second timeout (3 retries max) instead of the OS TCP timeout
- Connection runs on a background thread so the UI stays responsive, with a cancel option (press B or Escape) to back out at any time
- Failed connections now always show a "Connection Failed" dialog instead of silently navigating back
Upstream merge:
### Upstream Merge
- Fixed font rendering for color and formatting codes, splash text like "Colormatic!" now renders with proper per-character colors
- Fixed Sign editing UI, SignEntryMenu720 restored to correct version
- Stained glass and stained glass panes are now craftable in survival mode with full crafting UI support
- Clicking outside a container inventory while holding an item now drops it, matching Java Edition behavior
- Item lore text now displays on hover for items with NBT lore data
- Increased entity limits: boats 4060, minecarts 4060, fireballs 200300, projectiles 300400
- Increased entity limits: boats 40->60, minecarts 40->60, fireballs 200->300, projectiles 300->400
- Fixed missing trapped chest textures in Natural Texture Pack
- Debug packet handling now properly gated behind debug builds
Music context fixes:
- Menu music (menu1-4) now plays only on the title screen
- creative music (creative1-6) only plays in creative mode
- survival mode plays only calm/hal/nuance/piano tracks
### Music Context Fixes
- Menu music (menu1-4) now plays only on the title screen
- Creative music (creative1-6) only plays in creative mode
- Survival mode plays only calm/hal/nuance/piano tracks
### Performance Optimizations
Performance optimizations across rendering, audio, and entity systems!
- Renderer: column-level frustum culling and compact visible-chunk lists skip thousands of empty iterations per frame; lightweight second-pass render path avoids redundant checks
- Sound engine: filesystem probe results are now cached, eliminating repeated file-existence checks every time a sound plays; sounds are pre-decoded for smoother playback
- Entity movement: reduced `shared_from_this()` overhead by caching the shared pointer; `dynamic_pointer_cast` replaced with a raw pointer cast guarded by `instanceof`
@@ -126,12 +144,14 @@ Performance optimizations across rendering, audio, and entity systems!
- Threading: entity query locking consolidated at the `Level` layer on all platforms for consistent thread safety
- Block breaking: server now skips redundant tile-update packets when a block is successfully destroyed
Migrated to CMake build system (upstream)!
### CMake Build System Migration
- Project now builds with CMake instead of Visual Studio project files
- Use `cmake --preset windows64` or open the repo folder directly in Visual Studio (it detects `CMakeLists.txt` automatically)
- Old `.vcxproj`/`.sln` files are preserved on the `vs-build` branch if needed
Multi-language font rendering and Unicode text input!
### Multi-Language Font Rendering and Unicode Text Input
- Type and read text/characters in Japanese, Chinese, Korean, Thai, Arabic, Hindi, and many more languages
- Works in: chat, signs, world names, seeds, server address/port fields
- Two rendering systems: Iggy UI uses a new unicode bitmap fallback font; legacy C++ renderer uses Java Minecraft's glyph page system
@@ -139,19 +159,23 @@ Multi-language font rendering and Unicode text input!
- Security: blocked Unicode bidirectional override characters to prevent chat spoofing
- Fixed a pre-existing memory leak in sign loading
Added copy+paste support for IP/Port, world names, world seeds, server names, signs, etc.
- Just use control+v to paste from your clipboard!
### Copy+Paste Support
Dedicated server releases support Hardcore Mode!
- Dedicated server is fully compatible with `smartcmd/MinecraftConsoles` clients, even with hardcore mode!
- Added copy+paste support for IP/Port, world names, world seeds, server names, signs, etc.
- Just use Ctrl+V to paste from your clipboard
### Dedicated Server Hardcore Mode
- Dedicated server is fully compatible with `smartcmd/MinecraftConsoles` clients, even with hardcore mode
- Client (`LCREWindows64.zip`): download from the Nightly release on GitHub
- Dedicated Server (`LCREServerWindows64.zip`): download from the Nightly-Dedicated-Server release on GitHub
- Docker: pull `ghcr.io/itsrevela/minecraft-lce-dedicated-server:nightly` for server container
Screenshot functionality with F2!
- pressing F2 will save a screenshot to a `screenshots` folder in your root game directory
- works in any context: main menu, pause menu, settings, inventory, crafting, and during gameplay
- a local-only chat message is shown to the player when in-game
### Screenshot Functionality
- Pressing F2 will save a screenshot to a `screenshots` folder in your root game directory
- Works in any context: main menu, pause menu, settings, inventory, crafting, and during gameplay
- A local-only chat message is shown to the player when in-game
Proper implementation of Hardcore Mode in LCRE!
- difficulty slider included in create world menu

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!");
}
}

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
tools/DecompileAS.java Normal file
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() + "]");
}
}
}

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
tools/DumpSetHealthBC.java Normal file
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
tools/PatchHudABC.java Normal file
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;
}
}