Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/zero-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exchange, `.33` separate HP, `.32` weapon expansion, `.34` later co-op, all unde
| X1 specials | All eight native normal/charged attacks, Zero muzzle offsets and selected effect adjustments | Activation, energy, upgrade gate and both directions checked; remaining visual/state combinations need playtesting |
| Hurt/fades | Replacement tied to submitted OAM, including hidden blink frames and pause fades | Renderer checks cover the reported X flashes and menu transitions |
| HUD | Original X3 Z badge with its frame/palette | ROM-to-render comparison matched all 218 nontransparent badge pixels |
| READY | Original X1 stage-entry lettering and animation, red for Zero and blue for X | Native respawn captures checked for both identities; X matches original pixels |
| Pause/title | Original Zero body in pause and title cursor; title confirmation still shoots | Upgraded/unupgraded menus and native title shot checked |
| Life heads | **Original X 1-up artwork**, both menu and pickups | Owner explicitly rejected the custom Zero head; do not restore any custom version |
| Select exchange | Grounded idle X/Zero exchange using original blue/red teleport art; game tasks freeze | Both directions, held/midair Select, frozen live projectile and deterministic replay checked |
Expand All @@ -32,6 +33,19 @@ both games. It did not: the implementation uses bounded source investigation,
asset tables and integration hooks. This does not establish perfect X3 fidelity
or a complete campaign playthrough.

## READY appearance

Post-0.0.1 READY follow-up: X1 `$80:9A3E` creates actor `$1CE8`, class `$0A`;
`$81:F091` selects animation group `$19`, at the original screen center.
Its sequence at `$AF:AF76` forms letters with two-frame poses 0..11 and then
flashes poses 12/13. The compositor changes only the blue color ramp of that
actor during arrival state `$D3=2` when Zero is active. Original neutral
highlights, geometry, visibility, timing and other users of its palette stay
intact. This requires no extra assets, ROM patch, simulation hook or save field.
Private `MMX_ZERO_READY_ONLY=1` runtime checks reach READY through actual death
and respawn, validate red lettering for Zero and unchanged lettering for X,
and optionally save `.zero-ready.cap` / `.x-ready.cap` for visual review.

## Public distribution and asset provenance

The [source-ROM release contract](mod-source-roms.md) is mandatory for **all**
Expand Down
24 changes: 18 additions & 6 deletions src/mmx_renderer.c
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ static uint16_t background(const Ppu *p, const Raster *r, unsigned layer, int x,
static void sprite(const Ppu *p, const Raster *r, int x, int sy, unsigned attr, int size,
int y, MmxRenderView view, uint16_t *out, bool margins_only,
const MmxSpriteAsset *asset, unsigned raw_tile, int *object_color,
bool full_coordinates, unsigned zero_icon) {
bool full_coordinates, unsigned zero_icon, bool red_ready) {
int row = full_coordinates ? y - sy : (y - sy) & 255;
if (row < 0 || row >= size) return;
if (attr & 0x8000) row = size - 1 - row;
Expand Down Expand Up @@ -627,6 +627,14 @@ static void sprite(const Ppu *p, const Raster *r, int x, int sy, unsigned attr,
out[dest] = (uint16_t)(z | pixel);
object_color[dest] = hud_color >= 0 ? hud_color :
asset && !asset->live_colors ? asset->colors[pixel] : -1;
if (red_ready) {
unsigned color=object_color[dest]>=0 ? (unsigned)object_color[dest] : r->palette[(z|pixel)&255];
unsigned red=color&31,green=(color>>5)&31,blue=(color>>10)&31;
/* Keep the original highlights/neutral outline. Convert only the
* blue ramp to red, using its existing dark-to-light shading. */
if (blue>red && blue>green)
object_color[dest]=(int)(blue | ((red<green ? red : green)<<5) | (red<<10));
}
if (x + c < 0 || x + c >= 256) ++stats.margin_sprite_pixels;
}
}
Expand Down Expand Up @@ -787,7 +795,7 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) {
for (int i = (int)waiting_count - 1; i >= 0; --i) {
Piece s = waiting[i];
const MmxSpriteAsset *asset = s.animation == 0x53 ? waiting_zero : NULL;
sprite(&p, r, s.x, s.y, s.attr, s.size, y, view, objects, true, asset, s.tile, object_colors, true, false);
sprite(&p, r, s.x, s.y, s.attr, s.size, y, view, objects, true, asset, s.tile, object_colors, true, false, false);
}
bool replaced[128] = {false};
bool zero_drawn[2] = {false,false};
Expand All @@ -799,6 +807,10 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) {
* their entire footprint, including x=255 which native D76A clips.
* Only the explicit expanded list can add pieces beyond that budget. */
bool center = g_mmx_render_asset_repairs;
/* $80:9A3E spawns READY in $1CE8; $81:F091 selects group $19.
* Restrict the recolor to this actor during the arrival phase. */
bool red_ready=stage && MmxZeroEnabled() && !frame_zero.active_x &&
frame.ram[0xd3]==2 && s.object==0x1ce8 && s.animation==0x19;
bool menu_body = s.object == 0x1988 && (s.animation == 0 || s.animation == 0x18);
bool zero_body = zero && (s.object == 0xba8 || menu_body);
bool swap_actor = swapping && (s.object == 0xba8 || s.object == 0xc38 ||
Expand All @@ -807,7 +819,7 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) {
bool zero_armor = zero && (s.object == 0xc38 || s.object == 0xc58 || s.object == 0xc78 ||
(zero_menu && (s.object == 0x1928 || s.object == 0x1948 || s.object == 0x1968)));
bool oam_match = false;
if (g_mmx_render_asset_repairs || zero_body || zero_armor || zero_charge || swap_actor) for (int slot = 16; slot < 128; ++slot) {
if (g_mmx_render_asset_repairs || zero_body || zero_armor || zero_charge || swap_actor || red_ready) for (int slot = 16; slot < 128; ++slot) {
unsigned pos = r->oam[slot * 2], hi = r->high_oam[slot / 4] >> (slot % 4 * 2);
int ox = (pos & 255) | ((hi & 1) << 8); if (ox >= 256) ox -= 512;
if (ox == s.x && (pos >> 8) == ((unsigned)s.y & 255) && r->oam[slot * 2 + 1] == s.attr) {
Expand Down Expand Up @@ -864,7 +876,7 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) {
s.x += (s.x + s.size / 2 - cx) / 4;
s.y += (s.y + s.size / 2 - cy) / 4 - 6;
}
sprite(&p, r, s.x, s.y, attr, s.size, y, view, objects, !center, asset, s.tile, object_colors, true, false);
sprite(&p, r, s.x, s.y, attr, s.size, y, view, objects, !center, asset, s.tile, object_colors, true, false, red_ready);
}
int bar_first = -1, bar_count = 0;
if (hud) for (int slot = 16; slot <= 48; ++slot) {
Expand All @@ -890,7 +902,7 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) {
x == 8 && sy == 80 && attr == 0x3486 && size == 16;
bool anchored = stage && hud && sy < 96 && (slot < 16 || (bar_count >= 4 && slot >= bar_first && slot < bar_first + bar_count));
if (anchored) { if (x < 25) x -= view.extra; else if (x >= 216) x += view.extra; }
sprite(&p, r, x, sy, attr, size, y, view, objects, false, NULL, 0, object_colors, false, zero_icon);
sprite(&p, r, x, sy, attr, size, y, view, objects, false, NULL, 0, object_colors, false, zero_icon, false);
}
if (swapping) {
unsigned pose = MmxZeroSwapPose(&frame_zero);
Expand All @@ -913,7 +925,7 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) {
const uint8_t *layout = a ? sprite_arrangement(0,pose) : NULL;
if (layout) for (int i = layout[0] - 1; i >= 0; --i) {
Piece s = make_piece(layout + i * 4,x,sy,flip,0x22,0,0,0xba8);
sprite(&p,r,s.x,s.y,s.attr,s.size,y,view,objects,false,a,s.tile,object_colors,true,false);
sprite(&p,r,s.x,s.y,s.attr,s.size,y,view,objects,false,a,s.tile,object_colors,true,false,false);
}
}
}
Expand Down
33 changes: 33 additions & 0 deletions tests/mmx_adaptive_state_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,38 @@ static void zero_health_pickup(unsigned small) {
zero_replay(55);
check(!g_ram[0x1628],"native health pickup finishes collection");
}
static void zero_ready_checks(const char *fixture) {
for(unsigned character=0;character<2;++character) {
check(RtlLoadSnapshot(fixture),"restore READY fixture");
zero_replay(10);
if(character) zero_health_swap();
g_ram[0xbcf]=128; g_ram[0xbaa]=0x0c; g_ram[0xbab]=0;
bool seen=false;
for(unsigned i=0;i<1000;++i) {
frame(0);
if(g_ram[0xd3]==2 && g_ram[0x1ce9] && g_ram[0x1cfe]==0x19 && !g_ram[0x1cf4]) {
zero_replay(28); /* Group $19 pose 12: fully formed lettering. */
zero_capture(getenv("MMX_ZERO_TEST_CAPTURE"),character?".x-ready.cap":".zero-ready.cap");
uint32_t pixels[256*224];
check(MmxRendererDraw(pixels,(MmxRenderView){256,0,4.0/3.0},true),"READY frame renders");
const uint32_t *stock=MmxRendererStockFrame();
unsigned changed=0,red_pixels=0;
for(unsigned y=96;y<128;++y) for(unsigned x=80;x<176;++x) {
unsigned index=y*256+x,color=pixels[index];
if(color!=stock[index]) {
++changed;
if(((color>>16)&255)>(color&255)+40 && ((color>>16)&255)>((color>>8)&255)+40) ++red_pixels;
}
}
check(character ? !changed : changed>20 && changed==red_pixels,
"Zero READY changes only blue lettering to red; X READY matches original");
seen=true; break;
}
}
check(seen,"native respawn reaches the original READY banner");
}
puts("MMX READY CHECKS PASSED");
}
static void zero_health_checks(const char *fixture, uint8 *start, uint8 *expected, uint8 *actual, size_t cap) {
check(RtlLoadSnapshot(fixture),"restore for separate HP");
zero_replay(10);
Expand Down Expand Up @@ -343,6 +375,7 @@ static void zero_state_checks(const char *assets, const char *fixture, uint8 *st
g_config.widescreen = false;
int w,h; MmxPrepareFrame(1280,720,&w,&h);
check(w == 256 && g_mmx_custom_renderer, "Zero activates native-width compositor");
if(getenv("MMX_ZERO_READY_ONLY")) { zero_ready_checks(fixture); return; }
zero_swap_checks(fixture,start,expected,actual,cap);
if(getenv("MMX_ZERO_SWAP_ONLY")) { puts("MMX SELECT SWAP CHECKS PASSED"); return; }
zero_health_checks(fixture,start,expected,actual,cap);
Expand Down
Loading