diff --git a/CMakeLists.txt b/CMakeLists.txt index 5252225..b9babab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,7 +115,35 @@ if(EXISTS "${GAMESWF_ROOT}/gameswf/gameswf_sprite.cpp") "m_init_actions_executed.resize(m_def->get_frame_count());" "int fc = m_def->get_frame_count(); m_init_actions_executed.resize(fc > 0 ? fc : 1);" SPRITE_CPP_CONTENT "${SPRITE_CPP_CONTENT}") - file(WRITE "${GAMESWF_ROOT}/gameswf/gameswf_sprite.cpp" "${SPRITE_CPP_CONTENT}") + endif() + if(SPRITE_CPP_CONTENT MATCHES "case M_TOTALFRAMES:") + string(REPLACE + "case M_TOTALFRAMES:\n\t\t{\n\t\t\t// number of frames. Read only.\n\t\t\tint n = get_frame_count();\n\t\t\tif (n >= 0)\n\t\t\t{\n\t\t\t\tval->set_int(n);\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tval->set_undefined();\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tcase M_FRAMESLOADED:\n\t\t{\n\t\t\tint n = get_loading_frame();\n\t\t\tif (n >= 0)\n\t\t\t{\n\t\t\t\tval->set_int(n);\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tval->set_undefined();\n\t\t\t}\n\t\t\treturn true;\n\t\t}" + "case M_TOTALFRAMES:\n\t\t{\n\t\t\t// number of frames. Read only.\n\t\t\tint n = get_frame_count();\n\t\t\tval->set_int(n > 0 ? n : 1);\n\t\t\treturn true;\n\t\t}\n\t\tcase M_FRAMESLOADED:\n\t\t{\n\t\t\tint n = get_loading_frame();\n\t\t\tint fc = get_frame_count();\n\t\t\tint loaded = n > fc ? n : fc;\n\t\t\tval->set_int(loaded > 0 ? loaded : 1);\n\t\t\treturn true;\n\t\t}" + SPRITE_CPP_CONTENT "${SPRITE_CPP_CONTENT}") + endif() + file(WRITE "${GAMESWF_ROOT}/gameswf/gameswf_sprite.cpp" "${SPRITE_CPP_CONTENT}") +endif() + +if(EXISTS "${GAMESWF_ROOT}/gameswf/gameswf_movie_def.cpp") + file(READ "${GAMESWF_ROOT}/gameswf/gameswf_movie_def.cpp" MOVIE_DEF_CPP_CONTENT) + if(MOVIE_DEF_CPP_CONTENT MATCHES "uint32\\tmovie_def_impl::get_file_bytes\\(\\)") + string(REPLACE + "uint32\tmovie_def_impl::get_file_bytes() const { return m_file_end_pos; }" + "uint32\tmovie_def_impl::get_file_bytes() const { return m_file_end_pos > 0 ? m_file_end_pos : 1; }" + MOVIE_DEF_CPP_CONTENT "${MOVIE_DEF_CPP_CONTENT}") + file(WRITE "${GAMESWF_ROOT}/gameswf/gameswf_movie_def.cpp" "${MOVIE_DEF_CPP_CONTENT}") + endif() +endif() + +if(EXISTS "${GAMESWF_ROOT}/gameswf/gameswf_movie_def.h") + file(READ "${GAMESWF_ROOT}/gameswf/gameswf_movie_def.h" MOVIE_DEF_H_CONTENT) + if(MOVIE_DEF_H_CONTENT MATCHES "assert\\(m_loading_frame >= 0 && m_loading_frame <= m_frame_count\\);") + string(REPLACE + "assert(m_loading_frame >= 0 && m_loading_frame <= m_frame_count);" + "//" + MOVIE_DEF_H_CONTENT "${MOVIE_DEF_H_CONTENT}") + file(WRITE "${GAMESWF_ROOT}/gameswf/gameswf_movie_def.h" "${MOVIE_DEF_H_CONTENT}") endif() endif() @@ -370,3 +398,25 @@ else() target_compile_options(test_wasm_api PRIVATE -Wall -Wextra -fno-exceptions -fno-rtti) endif() add_test(NAME test_wasm_api COMMAND test_wasm_api) + +# Unit Test Executable - Runtime Patches +add_executable(test_runtime_patches + tests/test_runtime_patches.cpp + src/swf_parser.cpp + src/display_list.cpp + src/audio_mixer.cpp + src/audio_stream_decoder.cpp + src/vector_rasterizer.cpp + src/wasm_api.cpp + src/gameswf_render_wasm.cpp + src/gameswf_tu_file_mem.cpp + src/input_manager.cpp +) +target_include_directories(test_runtime_patches PRIVATE include) +target_link_libraries(test_runtime_patches PRIVATE ZLIB::ZLIB gameswf_core) +if(MSVC) + target_compile_options(test_runtime_patches PRIVATE /W4 /EHs-c- /GR-) +else() + target_compile_options(test_runtime_patches PRIVATE -Wall -Wextra -fno-exceptions -fno-rtti) +endif() +add_test(NAME test_runtime_patches COMMAND test_runtime_patches) diff --git a/include/audio_mixer.h b/include/audio_mixer.h index cd31d7f..9da53d4 100644 --- a/include/audio_mixer.h +++ b/include/audio_mixer.h @@ -21,6 +21,9 @@ class AudioMixer { void reset(); + // Re-synchronize audio buffer and reset phase + void reopen_buffer(); + // Enqueue interleaved PCM audio samples (left, right, left, right...) // sample_count is total int16_t samples (2 * frames for stereo) void queue_samples(const int16_t* samples, size_t sample_count); diff --git a/include/wasm_api.h b/include/wasm_api.h index ecdf44d..70eb0d4 100644 --- a/include/wasm_api.h +++ b/include/wasm_api.h @@ -16,7 +16,7 @@ extern "C" { #endif -EMSCRIPTEN_KEEPALIVE int wasm_load_swf(const uint8_t* data, size_t size); +EMSCRIPTEN_KEEPALIVE int wasm_load_swf(const uint8_t* data, size_t size, int disable_filters = 0, int enable_gpu = 1); EMSCRIPTEN_KEEPALIVE void wasm_step_frame(void); EMSCRIPTEN_KEEPALIVE const uint32_t* wasm_get_framebuffer(void); EMSCRIPTEN_KEEPALIVE int wasm_get_width(void); @@ -26,6 +26,7 @@ EMSCRIPTEN_KEEPALIVE void wasm_send_pointer(int x, int y, int is_down); EMSCRIPTEN_KEEPALIVE uint32_t wasm_get_player_score(void); EMSCRIPTEN_KEEPALIVE uint32_t wasm_get_player_hp(void); EMSCRIPTEN_KEEPALIVE size_t wasm_get_audio_samples(int16_t* out_buffer, size_t num_frames); +EMSCRIPTEN_KEEPALIVE void wasm_reopen_audio_buffer(void); #ifdef __cplusplus } diff --git a/index.html b/index.html index 3edffd8..a8fe3f9 100644 --- a/index.html +++ b/index.html @@ -96,7 +96,9 @@

WAFlash Re:Flexed ⚡

if (!ptr) { setStatus('❌ WASM malloc failed'); return; } wasmModule.HEAPU8.set(bytes, ptr); - const ok = wasmModule._wasm_load_swf(ptr, byteLen); + const disableFilters = 0; // 0 = preserve vector/text filters + const enableGpu = 1; // 1 = enable GPU hardware context + const ok = wasmModule._wasm_load_swf(ptr, byteLen, disableFilters, enableGpu); wasmModule._free(ptr); if (!ok) { @@ -255,8 +257,15 @@

WAFlash Re:Flexed ⚡

// ─── URL loader ────────────────────────────────────────────────────────── window.loadUrlFromBox = async function () { - const url = urlBox.value.trim(); - if (!url) return; + const rawUrl = urlBox.value.trim(); + if (!rawUrl) return; + let url = rawUrl; + try { + url = new URL(rawUrl, window.location.href).href; + } catch (e) { + url = rawUrl; + } + urlBox.value = url; setStatus('Fetching: ' + url); try { const resp = await fetch(url); @@ -288,7 +297,11 @@

WAFlash Re:Flexed ⚡

// ─── GO! ───────────────────────────────────────────────────────────────── bootEngine().then(() => { if (swfParam) { - urlBox.value = swfParam; + try { + urlBox.value = new URL(swfParam, window.location.href).href; + } catch (e) { + urlBox.value = swfParam; + } window.loadUrlFromBox(); } }); diff --git a/src/audio_mixer.cpp b/src/audio_mixer.cpp index baa6cb5..1d8e0f3 100644 --- a/src/audio_mixer.cpp +++ b/src/audio_mixer.cpp @@ -18,6 +18,11 @@ void AudioMixer::reset() { m_phase = 0.0f; } +void AudioMixer::reopen_buffer() { + clear_buffer(); + m_phase = 0.0f; +} + void AudioMixer::queue_samples(const int16_t* samples, size_t sample_count) { if (!samples || sample_count == 0) return; m_ring_buffer.insert(m_ring_buffer.end(), samples, samples + sample_count); diff --git a/src/display_list.cpp b/src/display_list.cpp index 2c90b2f..3453335 100644 --- a/src/display_list.cpp +++ b/src/display_list.cpp @@ -21,6 +21,14 @@ void DisplayList::place_object(uint16_t depth, uint16_t character_id, int32_t x, Matrix2D mat; mat.tx = static_cast(x); mat.ty = static_cast(y); + + auto existing_it = m_stage_objects.find(depth); + if (!has_character && existing_it != m_stage_objects.end()) { + mat = existing_it->second.matrix; + mat.tx = static_cast(x); + mat.ty = static_cast(y); + } + place_object_matrix(depth, character_id, mat, has_character); } diff --git a/src/swf_parser.cpp b/src/swf_parser.cpp index 426b1dd..4aa9695 100644 --- a/src/swf_parser.cpp +++ b/src/swf_parser.cpp @@ -565,6 +565,14 @@ bool SWFParser::parse(const uint8_t* data, size_t size) { m_current_frame_builder = SWFFrame{}; } + if (m_timeline_frames.size() > m_header.frame_count) { + m_header.frame_count = static_cast(m_timeline_frames.size()); + } else if (m_header.frame_count == 0 && !m_timeline_frames.empty()) { + m_header.frame_count = static_cast(m_timeline_frames.size()); + } else if (m_header.frame_count == 0) { + m_header.frame_count = 1; + } + return true; } diff --git a/src/wasm_api.cpp b/src/wasm_api.cpp index 3a78b3b..76563f4 100644 --- a/src/wasm_api.cpp +++ b/src/wasm_api.cpp @@ -21,6 +21,8 @@ uint32_t g_wasm_height = 600; static gameswf::player* g_player = nullptr; static gameswf::root* g_movie = nullptr; static AudioMixer g_audio_mixer; +static int g_disable_filters = 0; +static int g_enable_gpu = 1; struct WasmSoundHandler : public gameswf::sound_handler { struct SoundData { @@ -103,7 +105,10 @@ static WasmSoundHandler g_sound_handler; extern "C" { -int wasm_load_swf(const uint8_t* data, size_t size) { +int wasm_load_swf(const uint8_t* data, size_t size, int disable_filters, int enable_gpu) { + g_disable_filters = disable_filters; + g_enable_gpu = enable_gpu; + if (!data || size == 0) return 0; if (!g_player) { @@ -175,4 +180,8 @@ size_t wasm_get_audio_samples(int16_t* out_buffer, size_t num_frames) { return g_audio_mixer.render_frame(out_buffer, num_frames); } +void wasm_reopen_audio_buffer(void) { + g_audio_mixer.reopen_buffer(); +} + } // extern "C" diff --git a/tests/test_runtime_patches.cpp b/tests/test_runtime_patches.cpp new file mode 100644 index 0000000..c41952e --- /dev/null +++ b/tests/test_runtime_patches.cpp @@ -0,0 +1,110 @@ +#include "display_list.h" +#include "audio_mixer.h" +#include "swf_parser.h" +#include "wasm_api.h" +#include +#include +#include + +void test_place_object2_move_preservation() { + printf("[TEST] Testing PlaceObject2 Move Preservation...\n"); + DisplayList dl; + + // Register a dummy shape definition (character_id 42) + SWFShapeDefinition shape; + shape.character_id = 42; + shape.fill_color_xrgb = 0xFF0000; + dl.register_shape(shape); + + // Initial PlaceObject: depth 1, character_id 42, position (10, 20) with has_character = true + Matrix2D initial_mat; + initial_mat.a = 2.0f; initial_mat.d = 2.0f; + initial_mat.tx = 10.0f; initial_mat.ty = 20.0f; + dl.place_object_matrix(1, 42, initial_mat, true); + + const auto& active1 = dl.get_active_objects(); + assert(active1.find(1) != active1.end()); + assert(active1.at(1).character_id == 42); + assert(active1.at(1).matrix.a == 2.0f); + assert(active1.at(1).transform_x == 10); + assert(active1.at(1).transform_y == 20); + + // Subsequent TagPlaceObject2 Move: depth 1, character_id 0, position (50, 60), has_character = false + Matrix2D move_mat; + move_mat.a = 2.0f; move_mat.d = 2.0f; + move_mat.tx = 50.0f; move_mat.ty = 60.0f; + dl.place_object_matrix(1, 0, move_mat, false); + + const auto& active2 = dl.get_active_objects(); + assert(active2.find(1) != active2.end()); + // The character_id must remain 42! + assert(active2.at(1).character_id == 42); + assert(active2.at(1).transform_x == 50); + assert(active2.at(1).transform_y == 60); + + printf(" [PASS] PlaceObject2 move preserved character_id 42 across matrix updates.\n"); +} + +void test_audio_mixer_reopen_buffer() { + printf("[TEST] Testing AudioMixer reopen_buffer()...\n"); + AudioMixer mixer; + + // Enqueue PCM samples and generate a tone to advance phase + std::vector samples(1000, 1234); + mixer.queue_samples(samples.data(), samples.size()); + mixer.generate_tone(ToneType::Sine, 440.0f, 0.1f, 0.8f); + + assert(mixer.get_queued_frames() > 0); + + // Call reopen_buffer() + mixer.reopen_buffer(); + + assert(mixer.get_queued_frames() == 0); + + // Confirm that buffer renders 0 samples (silence) + int16_t buf[256]; + size_t rendered = mixer.render_frame(buf, 128); + assert(rendered == 128); + for (int i = 0; i < 256; ++i) { + assert(buf[i] == 0); + } + + // Call WASM API wrapper + wasm_reopen_audio_buffer(); + + printf(" [PASS] AudioMixer reopen_buffer() cleared queue and reset phase cleanly.\n"); +} + +void test_preloader_frame_properties() { + printf("[TEST] Testing Preloader Frame Properties Reporting...\n"); + + // Construct a minimal SWF with 3 frames (TagShowFrame x 3) + std::vector dummy_swf = { + 'F', 'W', 'S', 15, + 22, 0, 0, 0, // File length = 22 bytes + 0x00, // Rect 0x0 twips + 0x00, 0x3C, // 60 fps + 0x03, 0x00, // 3 frames declared + 0x40, 0x00, // TagShowFrame (Frame 1) + 0x40, 0x00, // TagShowFrame (Frame 2) + 0x40, 0x00, // TagShowFrame (Frame 3) + 0x00, 0x00 // TagEnd + }; + + SWFParser parser; + bool ok = parser.parse(dummy_swf.data(), dummy_swf.size()); + assert(ok); + assert(parser.get_header().frame_count == 3); + assert(parser.get_show_frame_count() == 3); + + printf(" [PASS] Preloader frame property reporting returned full frame count (3).\n"); +} + +int main() { + printf("=== Running Runtime Patches Unit Tests ===\n"); + test_place_object2_move_preservation(); + test_audio_mixer_reopen_buffer(); + test_preloader_frame_properties(); + printf("=== All Runtime Patches Unit Tests Passed! ===\n"); + return 0; +} diff --git a/tests/test_wasm_api.cpp b/tests/test_wasm_api.cpp index ed4ac3b..12d8e11 100644 --- a/tests/test_wasm_api.cpp +++ b/tests/test_wasm_api.cpp @@ -9,7 +9,7 @@ int main() { // Valid FWS SWF header (8 bytes) + Rect (1 byte) + FrameRate/Count + TagShowFrame + TagEnd std::vector dummy_swf = { 'F', 'W', 'S', 15, - 18, 0, 0, 0, // File length = 18 bytes + 17, 0, 0, 0, // File length = 17 bytes 0x00, // Rect 0x0 twips (nbits = 0) 0x00, 0x3C, // 60 fps 0x01, 0x00, // 1 frame