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