improve script hot reload
This commit is contained in:
parent
01d6e09a91
commit
15041984f1
25 changed files with 1516 additions and 293 deletions
|
|
@ -7,15 +7,32 @@
|
||||||
(var bass-params nil)
|
(var bass-params nil)
|
||||||
(var playing false)
|
(var playing false)
|
||||||
|
|
||||||
(local melody [60 64 67 72 67 64 60 64 67 72 76 72 67 64 62 66 69 74 69 66 62 66 69 74 78 74 69 66])
|
(local bpm 120)
|
||||||
(local bass [36 50 40 36 38 38 50 38])
|
(local beat (/ 60 bpm))
|
||||||
(local step-duration 0.15)
|
(local whole (* 4 beat))
|
||||||
|
(local half (* 2 beat))
|
||||||
|
(local quarter beat)
|
||||||
|
(local eighth (/ beat 2))
|
||||||
|
(local sixteenth (/ beat 4))
|
||||||
|
|
||||||
|
(local melody [[60 eighth] [64 eighth] [67 eighth] [72 eighth]
|
||||||
|
[67 eighth] [64 eighth] [60 eighth] [64 eighth]
|
||||||
|
[67 eighth] [72 eighth] [76 eighth] [72 eighth]
|
||||||
|
[67 eighth] [64 eighth] [62 eighth] [66 eighth]
|
||||||
|
[69 eighth] [74 eighth] [69 eighth] [66 eighth]
|
||||||
|
[62 eighth] [66 eighth] [69 eighth] [74 eighth]
|
||||||
|
[78 eighth] [74 eighth] [69 eighth] [66 eighth]])
|
||||||
|
|
||||||
|
; (local bass [[36 half] [40 half] [36 half] [38 half]
|
||||||
|
; [38 half] [36 half] [40 half] [38 half]])
|
||||||
|
|
||||||
|
(local step-duration eighth)
|
||||||
|
|
||||||
(fn init []
|
(fn init []
|
||||||
(set ctx (pxl8.sfx_context_create))
|
(set ctx (pxl8.sfx_context_create))
|
||||||
|
|
||||||
(local delay (pxl8.sfx_delay_create {:time_l 350 :time_r 500 :feedback 0.4 :mix 0.25}))
|
(local delay (pxl8.sfx_delay_create {:time_l 350 :time_r 500 :feedback 0.6 :mix 0.2}))
|
||||||
(local reverb (pxl8.sfx_reverb_create {:room 0.5 :damping 0.5 :mix 0.3}))
|
(local reverb (pxl8.sfx_reverb_create {:room 0.25 :damping 0.5 :mix 0.5}))
|
||||||
(local compressor (pxl8.sfx_compressor_create {:threshold -12 :ratio 4 :attack 10 :release 100}))
|
(local compressor (pxl8.sfx_compressor_create {:threshold -12 :ratio 4 :attack 10 :release 100}))
|
||||||
|
|
||||||
(pxl8.sfx_context_append_node ctx delay)
|
(pxl8.sfx_context_append_node ctx delay)
|
||||||
|
|
@ -26,19 +43,20 @@
|
||||||
|
|
||||||
(set params (pxl8.sfx_voice_params
|
(set params (pxl8.sfx_voice_params
|
||||||
{:waveform pxl8.SFX_WAVE_TRIANGLE
|
{:waveform pxl8.SFX_WAVE_TRIANGLE
|
||||||
:attack 0.005 :decay 0.08 :sustain 0.0 :release 0.05
|
:attack 0.01 :decay 0.25 :sustain 0.0 :release 0.01
|
||||||
:filter_type pxl8.SFX_FILTER_LOWPASS
|
:filter_type pxl8.SFX_FILTER_LOWPASS
|
||||||
:filter_cutoff 2500 :filter_resonance 0.15
|
:filter_cutoff 2500 :filter_resonance 0.05
|
||||||
:filter_attack 0.005 :filter_decay 0.1 :filter_sustain 0.1 :filter_release 0.05
|
:filter_attack 0.05 :filter_decay 0.1 :filter_sustain 0.2 :filter_release 0.05
|
||||||
:filter_env_depth 1500
|
:filter_env_depth 1500
|
||||||
:fx_send 0.3}))
|
:fx_send 0.7})))
|
||||||
|
|
||||||
(set bass-params (pxl8.sfx_voice_params
|
; (set bass-params (pxl8.sfx_voice_params
|
||||||
{:waveform pxl8.SFX_WAVE_SQUARE
|
; {:waveform pxl8.SFX_WAVE_SAW
|
||||||
:attack 0.005 :decay 0.1 :sustain 0.0 :release 0.05
|
; :attack 0.02 :decay 0.3 :sustain 0.6 :release 0.4
|
||||||
:filter_type pxl8.SFX_FILTER_LOWPASS
|
; :filter_type pxl8.SFX_FILTER_LOWPASS
|
||||||
:filter_cutoff 600 :filter_resonance 0.2
|
; :filter_attack 0.02 :filter_decay 0.3 :filter_sustain 0.6 :filter_release 0.4
|
||||||
:fx_send 0.2})))
|
; :filter_cutoff 90 :filter_resonance 0.25
|
||||||
|
; :fx_send 0.2})))
|
||||||
|
|
||||||
(fn start []
|
(fn start []
|
||||||
(set playing true)
|
(set playing true)
|
||||||
|
|
@ -54,11 +72,16 @@
|
||||||
(set time (+ time dt))
|
(set time (+ time dt))
|
||||||
(when (>= time step-duration)
|
(when (>= time step-duration)
|
||||||
(set time (- time step-duration))
|
(set time (- time step-duration))
|
||||||
(local note (. melody (+ 1 (% step (length melody)))))
|
(local melody-entry (. melody (+ 1 (% step (length melody)))))
|
||||||
(pxl8.sfx_play_note ctx note params 0.4)
|
(local note (. melody-entry 1))
|
||||||
(when (= (% step 4) 0)
|
(local dur (. melody-entry 2))
|
||||||
(local bass-note (. bass (+ 1 (% (math.floor (/ step 4)) (length bass)))))
|
(pxl8.sfx_play_note ctx note params 0.4 dur)
|
||||||
(pxl8.sfx_play_note ctx bass-note bass-params 0.35))
|
; (when (= (% step 4) 0)
|
||||||
|
; (local bass-idx (+ 1 (% (math.floor (/ step 4)) (length bass))))
|
||||||
|
; (local bass-entry (. bass bass-idx))
|
||||||
|
; (local bass-note (. bass-entry 1))
|
||||||
|
; (local bass-dur (. bass-entry 2))
|
||||||
|
; (pxl8.sfx_play_note ctx bass-note bass-params 0.35 bass-dur))
|
||||||
(set step (+ step 1)))))
|
(set step (+ step 1)))))
|
||||||
|
|
||||||
{:init init
|
{:init init
|
||||||
|
|
|
||||||
16
pxl8.sh
16
pxl8.sh
|
|
@ -344,6 +344,8 @@ case "$COMMAND" in
|
||||||
src/pxl8_log.c
|
src/pxl8_log.c
|
||||||
src/pxl8_math.c
|
src/pxl8_math.c
|
||||||
src/pxl8_repl.c
|
src/pxl8_repl.c
|
||||||
|
src/pxl8_replay.c
|
||||||
|
src/pxl8_rng.c
|
||||||
src/pxl8_save.c
|
src/pxl8_save.c
|
||||||
src/pxl8_script.c
|
src/pxl8_script.c
|
||||||
src/pxl8_sdl3.c
|
src/pxl8_sdl3.c
|
||||||
|
|
@ -380,9 +382,23 @@ case "$COMMAND" in
|
||||||
obj_file="$OBJECT_DIR/$obj_name"
|
obj_file="$OBJECT_DIR/$obj_name"
|
||||||
OBJECTS="$OBJECTS $obj_file"
|
OBJECTS="$OBJECTS $obj_file"
|
||||||
|
|
||||||
|
NEEDS_REBUILD=false
|
||||||
if [[ "$src_file" -nt "$obj_file" ]] || \
|
if [[ "$src_file" -nt "$obj_file" ]] || \
|
||||||
[[ "src/pxl8_types.h" -nt "$obj_file" ]] || \
|
[[ "src/pxl8_types.h" -nt "$obj_file" ]] || \
|
||||||
[[ "src/pxl8_macros.h" -nt "$obj_file" ]]; then
|
[[ "src/pxl8_macros.h" -nt "$obj_file" ]]; then
|
||||||
|
NEEDS_REBUILD=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$src_file" == "src/pxl8_script.c" ]]; then
|
||||||
|
for lua_file in src/lua/*.lua src/lua/pxl8/*.lua lib/fennel/fennel.lua; do
|
||||||
|
if [[ -f "$lua_file" ]] && [[ "$lua_file" -nt "$obj_file" ]]; then
|
||||||
|
NEEDS_REBUILD=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$NEEDS_REBUILD" == true ]]; then
|
||||||
NEED_LINK=true
|
NEED_LINK=true
|
||||||
compile_source_file "$src_file" "$obj_file" "$COMPILE_FLAGS"
|
compile_source_file "$src_file" "$obj_file" "$COMPILE_FLAGS"
|
||||||
SOURCES_COMPILED="yes"
|
SOURCES_COMPILED="yes"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ local transition = require("pxl8.transition")
|
||||||
local anim = require("pxl8.anim")
|
local anim = require("pxl8.anim")
|
||||||
local sfx = require("pxl8.sfx")
|
local sfx = require("pxl8.sfx")
|
||||||
|
|
||||||
core.init(_pxl8_gfx, _pxl8_input, _pxl8_sfx_mixer, _pxl8_sys)
|
core.init(_pxl8_gfx, _pxl8_input, _pxl8_rng, _pxl8_sfx_mixer, _pxl8_sys)
|
||||||
|
|
||||||
local pxl8 = {}
|
local pxl8 = {}
|
||||||
|
|
||||||
|
|
@ -29,6 +29,11 @@ pxl8.debug = core.debug
|
||||||
pxl8.trace = core.trace
|
pxl8.trace = core.trace
|
||||||
pxl8.quit = core.quit
|
pxl8.quit = core.quit
|
||||||
|
|
||||||
|
pxl8.rng_seed = core.rng_seed
|
||||||
|
pxl8.rng_next = core.rng_next
|
||||||
|
pxl8.rng_f32 = core.rng_f32
|
||||||
|
pxl8.rng_range = core.rng_range
|
||||||
|
|
||||||
pxl8.clear = gfx2d.clear
|
pxl8.clear = gfx2d.clear
|
||||||
pxl8.pixel = gfx2d.pixel
|
pxl8.pixel = gfx2d.pixel
|
||||||
pxl8.line = gfx2d.line
|
pxl8.line = gfx2d.line
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ local C = ffi.C
|
||||||
|
|
||||||
local core = {}
|
local core = {}
|
||||||
|
|
||||||
function core.init(gfx, input, sfx_mixer, sys)
|
function core.init(gfx, input, rng, sfx_mixer, sys)
|
||||||
core.gfx = gfx
|
core.gfx = gfx
|
||||||
core.input = input
|
core.input = input
|
||||||
|
core.rng = rng
|
||||||
core.sfx_mixer = sfx_mixer
|
core.sfx_mixer = sfx_mixer
|
||||||
core.sys = sys
|
core.sys = sys
|
||||||
end
|
end
|
||||||
|
|
@ -72,4 +73,20 @@ function core.quit()
|
||||||
C.pxl8_set_running(core.sys, false)
|
C.pxl8_set_running(core.sys, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function core.rng_seed(seed)
|
||||||
|
C.pxl8_rng_seed(core.rng, seed)
|
||||||
|
end
|
||||||
|
|
||||||
|
function core.rng_next()
|
||||||
|
return C.pxl8_rng_next(core.rng)
|
||||||
|
end
|
||||||
|
|
||||||
|
function core.rng_f32()
|
||||||
|
return C.pxl8_rng_f32(core.rng)
|
||||||
|
end
|
||||||
|
|
||||||
|
function core.rng_range(min, max)
|
||||||
|
return C.pxl8_rng_range(core.rng, min, max)
|
||||||
|
end
|
||||||
|
|
||||||
return core
|
return core
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ local core = require("pxl8.core")
|
||||||
local particles = {}
|
local particles = {}
|
||||||
|
|
||||||
function particles.new(max_count)
|
function particles.new(max_count)
|
||||||
return C.pxl8_particles_create(max_count or 1000)
|
return C.pxl8_particles_create(max_count or 1000, core.rng)
|
||||||
end
|
end
|
||||||
|
|
||||||
function particles.destroy(ps)
|
function particles.destroy(ps)
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,8 @@ function sfx.note_to_freq(note)
|
||||||
return C.pxl8_sfx_note_to_freq(note)
|
return C.pxl8_sfx_note_to_freq(note)
|
||||||
end
|
end
|
||||||
|
|
||||||
function sfx.play_note(ctx, note, params, volume)
|
function sfx.play_note(ctx, note, params, volume, duration)
|
||||||
return C.pxl8_sfx_play_note(ctx, note, params, volume or 0.8)
|
return C.pxl8_sfx_play_note(ctx, note, params, volume or 0.8, duration or 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
function sfx.release_voice(ctx, voice_id)
|
function sfx.release_voice(ctx, voice_id)
|
||||||
|
|
|
||||||
51
src/pxl8.c
51
src/pxl8.c
|
|
@ -15,7 +15,9 @@
|
||||||
#include "pxl8_log.h"
|
#include "pxl8_log.h"
|
||||||
#include "pxl8_macros.h"
|
#include "pxl8_macros.h"
|
||||||
#include "pxl8_repl.h"
|
#include "pxl8_repl.h"
|
||||||
|
#include "pxl8_replay.h"
|
||||||
#include "pxl8_script.h"
|
#include "pxl8_script.h"
|
||||||
|
#include "pxl8_sfx.h"
|
||||||
#include "pxl8_sys.h"
|
#include "pxl8_sys.h"
|
||||||
|
|
||||||
struct pxl8 {
|
struct pxl8 {
|
||||||
|
|
@ -27,6 +29,15 @@ struct pxl8 {
|
||||||
void* platform_data;
|
void* platform_data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
static void pxl8_audio_event_callback(u8 event_type, u8 context_id, u8 note, f32 volume, void* userdata) {
|
||||||
|
pxl8_game* game = (pxl8_game*)userdata;
|
||||||
|
if (game && game->debug_replay) {
|
||||||
|
pxl8_replay_write_audio_event(game->debug_replay, game->frame_count, event_type, context_id, note, volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
pxl8* pxl8_create(const pxl8_hal* hal) {
|
pxl8* pxl8_create(const pxl8_hal* hal) {
|
||||||
pxl8* sys = (pxl8*)calloc(1, sizeof(pxl8));
|
pxl8* sys = (pxl8*)calloc(1, sizeof(pxl8));
|
||||||
if (!sys) return NULL;
|
if (!sys) return NULL;
|
||||||
|
|
@ -218,18 +229,26 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
|
||||||
return PXL8_ERROR_INITIALIZATION_FAILED;
|
return PXL8_ERROR_INITIALIZATION_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
game->mixer = pxl8_sfx_mixer_create();
|
game->mixer = pxl8_sfx_mixer_create(sys->hal);
|
||||||
if (!game->mixer) {
|
if (!game->mixer) {
|
||||||
pxl8_error("failed to create audio mixer");
|
pxl8_error("failed to create audio mixer");
|
||||||
return PXL8_ERROR_INITIALIZATION_FAILED;
|
return PXL8_ERROR_INITIALIZATION_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pxl8_rng_seed(&game->rng, (u32)sys->hal->get_ticks());
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
game->debug_replay = pxl8_replay_create_buffer(60, 60);
|
||||||
|
pxl8_sfx_mixer_set_event_callback(game->mixer, pxl8_audio_event_callback, game);
|
||||||
|
#endif
|
||||||
|
|
||||||
if (game->repl_mode) {
|
if (game->repl_mode) {
|
||||||
pxl8_info("starting in REPL mode with script: %s", game->script_path);
|
pxl8_info("starting in REPL mode with script: %s", game->script_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_script_set_gfx(game->script, game->gfx);
|
pxl8_script_set_gfx(game->script, game->gfx);
|
||||||
pxl8_script_set_input(game->script, &game->input);
|
pxl8_script_set_input(game->script, &game->input);
|
||||||
|
pxl8_script_set_rng(game->script, &game->rng);
|
||||||
pxl8_script_set_sfx(game->script, game->mixer);
|
pxl8_script_set_sfx(game->script, game->mixer);
|
||||||
pxl8_script_set_sys(game->script, sys);
|
pxl8_script_set_sys(game->script, sys);
|
||||||
|
|
||||||
|
|
@ -271,7 +290,18 @@ pxl8_result pxl8_update(pxl8* sys) {
|
||||||
game->fps_frame_count = 0;
|
game->fps_frame_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_script_check_reload(game->script);
|
#ifndef NDEBUG
|
||||||
|
u32 rng_state_before_reload = game->rng.state;
|
||||||
|
#endif
|
||||||
|
bool reloaded = pxl8_script_check_reload(game->script);
|
||||||
|
#ifndef NDEBUG
|
||||||
|
if (reloaded) {
|
||||||
|
game->rng.state = rng_state_before_reload;
|
||||||
|
pxl8_debug("Hot-reload: restored RNG state 0x%08X", rng_state_before_reload);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void)reloaded;
|
||||||
|
#endif
|
||||||
|
|
||||||
if (game->repl_mode && !game->repl_started) {
|
if (game->repl_mode && !game->repl_started) {
|
||||||
if (game->script_loaded) {
|
if (game->script_loaded) {
|
||||||
|
|
@ -309,6 +339,7 @@ pxl8_result pxl8_update(pxl8* sys) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_gfx_update(game->gfx, dt);
|
pxl8_gfx_update(game->gfx, dt);
|
||||||
|
pxl8_sfx_mixer_process(game->mixer);
|
||||||
|
|
||||||
if (game->script_loaded) {
|
if (game->script_loaded) {
|
||||||
pxl8_script_call_function_f32(game->script, "update", dt);
|
pxl8_script_call_function_f32(game->script, "update", dt);
|
||||||
|
|
@ -352,6 +383,18 @@ pxl8_result pxl8_frame(pxl8* sys) {
|
||||||
memset(game->input.mouse_buttons_released, 0, sizeof(game->input.mouse_buttons_released));
|
memset(game->input.mouse_buttons_released, 0, sizeof(game->input.mouse_buttons_released));
|
||||||
|
|
||||||
game->frame_count++;
|
game->frame_count++;
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
if (game->debug_replay) {
|
||||||
|
if (game->frame_count % 60 == 0) {
|
||||||
|
pxl8_replay_write_keyframe(game->debug_replay, game->frame_count, game->time, &game->rng, &game->input);
|
||||||
|
} else {
|
||||||
|
pxl8_replay_write_input(game->debug_replay, game->frame_count, &game->prev_input, &game->input);
|
||||||
|
}
|
||||||
|
game->prev_input = game->input;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
game->input.mouse_dx = 0;
|
game->input.mouse_dx = 0;
|
||||||
game->input.mouse_dy = 0;
|
game->input.mouse_dy = 0;
|
||||||
game->input.mouse_wheel_x = 0;
|
game->input.mouse_wheel_x = 0;
|
||||||
|
|
@ -371,6 +414,10 @@ void pxl8_quit(pxl8* sys) {
|
||||||
pxl8_cart_unmount(sys->cart);
|
pxl8_cart_unmount(sys->cart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
pxl8_replay_destroy(game->debug_replay);
|
||||||
|
#endif
|
||||||
|
|
||||||
pxl8_sfx_mixer_destroy(game->mixer);
|
pxl8_sfx_mixer_destroy(game->mixer);
|
||||||
pxl8_gfx_destroy(game->gfx);
|
pxl8_gfx_destroy(game->gfx);
|
||||||
pxl8_script_destroy(game->script);
|
pxl8_script_destroy(game->script);
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@ typedef struct pxl8_anim_state_machine {
|
||||||
u16 current_state;
|
u16 current_state;
|
||||||
} pxl8_anim_state_machine;
|
} pxl8_anim_state_machine;
|
||||||
|
|
||||||
|
static inline pxl8_anim* pxl8_anim_get_active(pxl8_anim* anim) {
|
||||||
|
if (anim->state_machine &&
|
||||||
|
anim->state_machine->current_state < anim->state_machine->state_count) {
|
||||||
|
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
|
||||||
|
if (state_anim) return state_anim;
|
||||||
|
}
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
|
||||||
pxl8_anim* pxl8_anim_create(const u32* frame_ids, const u16* frame_durations, u16 frame_count) {
|
pxl8_anim* pxl8_anim_create(const u32* frame_ids, const u16* frame_durations, u16 frame_count) {
|
||||||
if (!frame_ids || frame_count == 0) {
|
if (!frame_ids || frame_count == 0) {
|
||||||
pxl8_error("Invalid animation parameters");
|
pxl8_error("Invalid animation parameters");
|
||||||
|
|
@ -299,61 +308,26 @@ void pxl8_anim_reset(pxl8_anim* anim) {
|
||||||
|
|
||||||
void pxl8_anim_set_frame(pxl8_anim* anim, u16 frame) {
|
void pxl8_anim_set_frame(pxl8_anim* anim, u16 frame) {
|
||||||
if (!anim) return;
|
if (!anim) return;
|
||||||
|
pxl8_anim* target = pxl8_anim_get_active(anim);
|
||||||
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
|
if (frame < target->frame_count) {
|
||||||
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
|
target->current_frame = frame;
|
||||||
if (state_anim) {
|
target->time_accumulator = 0.0f;
|
||||||
pxl8_anim_set_frame(state_anim, frame);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame < anim->frame_count) {
|
|
||||||
anim->current_frame = frame;
|
|
||||||
anim->time_accumulator = 0.0f;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_anim_set_loop(pxl8_anim* anim, bool loop) {
|
void pxl8_anim_set_loop(pxl8_anim* anim, bool loop) {
|
||||||
if (!anim) return;
|
if (!anim) return;
|
||||||
|
pxl8_anim_get_active(anim)->loop = loop;
|
||||||
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
|
|
||||||
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
|
|
||||||
if (state_anim) {
|
|
||||||
state_anim->loop = loop;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anim->loop = loop;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_anim_set_reverse(pxl8_anim* anim, bool reverse) {
|
void pxl8_anim_set_reverse(pxl8_anim* anim, bool reverse) {
|
||||||
if (!anim) return;
|
if (!anim) return;
|
||||||
|
pxl8_anim_get_active(anim)->reverse = reverse;
|
||||||
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
|
|
||||||
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
|
|
||||||
if (state_anim) {
|
|
||||||
state_anim->reverse = reverse;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anim->reverse = reverse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_anim_set_speed(pxl8_anim* anim, f32 speed) {
|
void pxl8_anim_set_speed(pxl8_anim* anim, f32 speed) {
|
||||||
if (!anim) return;
|
if (!anim) return;
|
||||||
|
pxl8_anim_get_active(anim)->speed = speed;
|
||||||
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
|
|
||||||
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
|
|
||||||
if (state_anim) {
|
|
||||||
state_anim->speed = speed;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anim->speed = speed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_result pxl8_anim_set_state(pxl8_anim* anim, const char* name) {
|
pxl8_result pxl8_anim_set_state(pxl8_anim* anim, const char* name) {
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,12 @@ void pxl8_blit_hicolor(u16* fb, u32 fb_width, const u16* sprite, u32 atlas_width
|
||||||
col += 2;
|
col += 2;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
u16 s0 = (u16)(pixels);
|
dest_row[col] = pxl8_blend_hicolor((u16)(pixels), dest_row[col]);
|
||||||
u16 s1 = (u16)(pixels >> 16);
|
dest_row[col + 1] = pxl8_blend_hicolor((u16)(pixels >> 16), dest_row[col + 1]);
|
||||||
u16 d0 = dest_row[col];
|
|
||||||
u16 d1 = dest_row[col + 1];
|
|
||||||
u16 m0 = (u16)(-(s0 != 0));
|
|
||||||
u16 m1 = (u16)(-(s1 != 0));
|
|
||||||
dest_row[col] = (s0 & m0) | (d0 & ~m0);
|
|
||||||
dest_row[col + 1] = (s1 & m1) | (d1 & ~m1);
|
|
||||||
col += 2;
|
col += 2;
|
||||||
}
|
}
|
||||||
if (w & 1) {
|
if (w & 1) {
|
||||||
u16 s = src_row[col];
|
dest_row[col] = pxl8_blend_hicolor(src_row[col], dest_row[col]);
|
||||||
u16 d = dest_row[col];
|
|
||||||
u16 m = (u16)(-(s != 0));
|
|
||||||
dest_row[col] = (s & m) | (d & ~m);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,29 +44,14 @@ void pxl8_blit_indexed(u8* fb, u32 fb_width, const u8* sprite, u32 atlas_width,
|
||||||
col += 4;
|
col += 4;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
u8 s0 = (u8)(pixels);
|
dest_row[col] = pxl8_blend_indexed((u8)(pixels), dest_row[col]);
|
||||||
u8 s1 = (u8)(pixels >> 8);
|
dest_row[col + 1] = pxl8_blend_indexed((u8)(pixels >> 8), dest_row[col + 1]);
|
||||||
u8 s2 = (u8)(pixels >> 16);
|
dest_row[col + 2] = pxl8_blend_indexed((u8)(pixels >> 16), dest_row[col + 2]);
|
||||||
u8 s3 = (u8)(pixels >> 24);
|
dest_row[col + 3] = pxl8_blend_indexed((u8)(pixels >> 24), dest_row[col + 3]);
|
||||||
u8 d0 = dest_row[col];
|
|
||||||
u8 d1 = dest_row[col + 1];
|
|
||||||
u8 d2 = dest_row[col + 2];
|
|
||||||
u8 d3 = dest_row[col + 3];
|
|
||||||
u8 m0 = (u8)(-(s0 != 0));
|
|
||||||
u8 m1 = (u8)(-(s1 != 0));
|
|
||||||
u8 m2 = (u8)(-(s2 != 0));
|
|
||||||
u8 m3 = (u8)(-(s3 != 0));
|
|
||||||
dest_row[col] = (s0 & m0) | (d0 & ~m0);
|
|
||||||
dest_row[col + 1] = (s1 & m1) | (d1 & ~m1);
|
|
||||||
dest_row[col + 2] = (s2 & m2) | (d2 & ~m2);
|
|
||||||
dest_row[col + 3] = (s3 & m3) | (d3 & ~m3);
|
|
||||||
col += 4;
|
col += 4;
|
||||||
}
|
}
|
||||||
for (; col < w; col++) {
|
for (; col < w; col++) {
|
||||||
u8 s = src_row[col];
|
dest_row[col] = pxl8_blend_indexed(src_row[col], dest_row[col]);
|
||||||
u8 d = dest_row[col];
|
|
||||||
u8 m = (u8)(-(s != 0));
|
|
||||||
dest_row[col] = (s & m) | (d & ~m);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,16 @@
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static inline u8 pxl8_blend_indexed(u8 src, u8 dst) {
|
||||||
|
u8 m = (u8)(-(src != 0));
|
||||||
|
return (src & m) | (dst & ~m);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline u16 pxl8_blend_hicolor(u16 src, u16 dst) {
|
||||||
|
u16 m = (u16)(-(src != 0));
|
||||||
|
return (src & m) | (dst & ~m);
|
||||||
|
}
|
||||||
|
|
||||||
void pxl8_blit_hicolor(
|
void pxl8_blit_hicolor(
|
||||||
u16* fb, u32 fb_width,
|
u16* fb, u32 fb_width,
|
||||||
const u16* sprite, u32 atlas_width,
|
const u16* sprite, u32 atlas_width,
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "pxl8_gfx.h"
|
#include "pxl8_gfx.h"
|
||||||
|
#include "pxl8_rng.h"
|
||||||
#include "pxl8_script.h"
|
#include "pxl8_script.h"
|
||||||
#include "pxl8_sfx.h"
|
#include "pxl8_sfx.h"
|
||||||
#include "pxl8_types.h"
|
#include "pxl8_types.h"
|
||||||
|
|
||||||
|
typedef struct pxl8_replay pxl8_replay;
|
||||||
|
|
||||||
typedef struct pxl8_game {
|
typedef struct pxl8_game {
|
||||||
pxl8_gfx* gfx;
|
pxl8_gfx* gfx;
|
||||||
pxl8_script* script;
|
pxl8_script* script;
|
||||||
pxl8_sfx_mixer* mixer;
|
pxl8_sfx_mixer* mixer;
|
||||||
|
|
||||||
|
pxl8_rng rng;
|
||||||
i32 frame_count;
|
i32 frame_count;
|
||||||
u64 last_time;
|
u64 last_time;
|
||||||
f32 time;
|
f32 time;
|
||||||
|
|
@ -19,6 +23,11 @@ typedef struct pxl8_game {
|
||||||
f32 fps;
|
f32 fps;
|
||||||
|
|
||||||
pxl8_input_state input;
|
pxl8_input_state input;
|
||||||
|
pxl8_input_state prev_input;
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
pxl8_replay* debug_replay;
|
||||||
|
#endif
|
||||||
|
|
||||||
bool repl_mode;
|
bool repl_mode;
|
||||||
bool repl_started;
|
bool repl_started;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "pxl8_log.h"
|
#include "pxl8_log.h"
|
||||||
|
#include "pxl8_rng.h"
|
||||||
|
|
||||||
typedef struct room_grid {
|
typedef struct room_grid {
|
||||||
u8* cells;
|
u8* cells;
|
||||||
|
|
@ -11,19 +12,6 @@ typedef struct room_grid {
|
||||||
i32 height;
|
i32 height;
|
||||||
} room_grid;
|
} room_grid;
|
||||||
|
|
||||||
static u32 prng_state = 0;
|
|
||||||
|
|
||||||
static void prng_seed(u32 seed) {
|
|
||||||
prng_state = seed;
|
|
||||||
}
|
|
||||||
|
|
||||||
static u32 prng_next(void) {
|
|
||||||
prng_state ^= prng_state << 13;
|
|
||||||
prng_state ^= prng_state >> 17;
|
|
||||||
prng_state ^= prng_state << 5;
|
|
||||||
return prng_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool room_grid_init(room_grid* grid, i32 width, i32 height) {
|
static bool room_grid_init(room_grid* grid, i32 width, i32 height) {
|
||||||
grid->width = width;
|
grid->width = width;
|
||||||
grid->height = height;
|
grid->height = height;
|
||||||
|
|
@ -345,7 +333,8 @@ static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* param
|
||||||
params->width, params->height, params->seed,
|
params->width, params->height, params->seed,
|
||||||
params->min_room_size, params->max_room_size, params->num_rooms);
|
params->min_room_size, params->max_room_size, params->num_rooms);
|
||||||
|
|
||||||
prng_seed(params->seed);
|
pxl8_rng rng;
|
||||||
|
pxl8_rng_seed(&rng, params->seed);
|
||||||
|
|
||||||
room_grid grid;
|
room_grid grid;
|
||||||
if (!room_grid_init(&grid, params->width, params->height)) {
|
if (!room_grid_init(&grid, params->width, params->height)) {
|
||||||
|
|
@ -360,10 +349,10 @@ static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* param
|
||||||
i32 max_attempts = params->num_rooms * 10;
|
i32 max_attempts = params->num_rooms * 10;
|
||||||
|
|
||||||
for (i32 attempt = 0; attempt < max_attempts && room_count < params->num_rooms && room_count < 256; attempt++) {
|
for (i32 attempt = 0; attempt < max_attempts && room_count < params->num_rooms && room_count < 256; attempt++) {
|
||||||
i32 w = params->min_room_size + (prng_next() % (params->max_room_size - params->min_room_size + 1));
|
i32 w = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1));
|
||||||
i32 h = params->min_room_size + (prng_next() % (params->max_room_size - params->min_room_size + 1));
|
i32 h = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1));
|
||||||
i32 x = 1 + (prng_next() % (params->width - w - 2));
|
i32 x = 1 + (pxl8_rng_next(&rng) % (params->width - w - 2));
|
||||||
i32 y = 1 + (prng_next() % (params->height - h - 2));
|
i32 y = 1 + (pxl8_rng_next(&rng) % (params->height - h - 2));
|
||||||
|
|
||||||
pxl8_bounds new_room = {x, y, w, h};
|
pxl8_bounds new_room = {x, y, w, h};
|
||||||
|
|
||||||
|
|
@ -388,7 +377,7 @@ static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* param
|
||||||
i32 prev_cx = rooms[room_count - 1].x + rooms[room_count - 1].w / 2;
|
i32 prev_cx = rooms[room_count - 1].x + rooms[room_count - 1].w / 2;
|
||||||
i32 prev_cy = rooms[room_count - 1].y + rooms[room_count - 1].h / 2;
|
i32 prev_cy = rooms[room_count - 1].y + rooms[room_count - 1].h / 2;
|
||||||
|
|
||||||
if (prng_next() % 2 == 0) {
|
if (pxl8_rng_next(&rng) % 2 == 0) {
|
||||||
carve_corridor_h(&grid, prev_cx, new_cx, prev_cy);
|
carve_corridor_h(&grid, prev_cx, new_cx, prev_cy);
|
||||||
carve_corridor_v(&grid, prev_cy, new_cy, new_cx);
|
carve_corridor_v(&grid, prev_cy, new_cy, new_cx);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -440,8 +429,6 @@ static u32 hash2d(i32 x, i32 y) {
|
||||||
void pxl8_procgen_tex(u8* buffer, const pxl8_procgen_tex_params* params) {
|
void pxl8_procgen_tex(u8* buffer, const pxl8_procgen_tex_params* params) {
|
||||||
if (!buffer || !params) return;
|
if (!buffer || !params) return;
|
||||||
|
|
||||||
prng_seed(params->seed);
|
|
||||||
|
|
||||||
for (i32 y = 0; y < params->height; y++) {
|
for (i32 y = 0; y < params->height; y++) {
|
||||||
for (i32 x = 0; x < params->width; x++) {
|
for (i32 x = 0; x < params->width; x++) {
|
||||||
f32 u = (f32)x / (f32)params->width;
|
f32 u = (f32)x / (f32)params->width;
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,19 @@ void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom) {
|
||||||
(void)gfx; (void)left; (void)right; (void)top; (void)bottom;
|
(void)gfx; (void)left; (void)right; (void)top; (void)bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline void pxl8_fb_set(pxl8_gfx* gfx, i32 idx, u32 color) {
|
||||||
|
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR)
|
||||||
|
((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color);
|
||||||
|
else
|
||||||
|
gfx->framebuffer[idx] = color & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline u32 pxl8_fb_get(pxl8_gfx* gfx, i32 idx) {
|
||||||
|
return (gfx->pixel_mode == PXL8_PIXEL_HICOLOR)
|
||||||
|
? pxl8_rgb565_to_rgba32(((u16*)gfx->framebuffer)[idx])
|
||||||
|
: gfx->framebuffer[idx];
|
||||||
|
}
|
||||||
|
|
||||||
void pxl8_clear(pxl8_gfx* gfx, u32 color) {
|
void pxl8_clear(pxl8_gfx* gfx, u32 color) {
|
||||||
if (!gfx || !gfx->framebuffer) return;
|
if (!gfx || !gfx->framebuffer) return;
|
||||||
|
|
||||||
|
|
@ -403,25 +416,13 @@ void pxl8_clear(pxl8_gfx* gfx, u32 color) {
|
||||||
void pxl8_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color) {
|
void pxl8_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color) {
|
||||||
if (!gfx || !gfx->framebuffer) return;
|
if (!gfx || !gfx->framebuffer) return;
|
||||||
if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return;
|
if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return;
|
||||||
|
pxl8_fb_set(gfx, y * gfx->framebuffer_width + x, color);
|
||||||
i32 idx = y * gfx->framebuffer_width + x;
|
|
||||||
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
|
|
||||||
((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color);
|
|
||||||
} else {
|
|
||||||
gfx->framebuffer[idx] = color & 0xFF;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 pxl8_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) {
|
u32 pxl8_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) {
|
||||||
if (!gfx || !gfx->framebuffer) return 0;
|
if (!gfx || !gfx->framebuffer) return 0;
|
||||||
if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return 0;
|
if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return 0;
|
||||||
|
return pxl8_fb_get(gfx, y * gfx->framebuffer_width + x);
|
||||||
i32 idx = y * gfx->framebuffer_width + x;
|
|
||||||
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
|
|
||||||
return pxl8_rgb565_to_rgba32(((u16*)gfx->framebuffer)[idx]);
|
|
||||||
} else {
|
|
||||||
return gfx->framebuffer[idx];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_line(pxl8_gfx* gfx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color) {
|
void pxl8_line(pxl8_gfx* gfx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color) {
|
||||||
|
|
@ -460,12 +461,7 @@ void pxl8_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void pxl8_pixel_unchecked(pxl8_gfx* gfx, i32 x, i32 y, u32 color) {
|
static inline void pxl8_pixel_unchecked(pxl8_gfx* gfx, i32 x, i32 y, u32 color) {
|
||||||
i32 idx = y * gfx->framebuffer_width + x;
|
pxl8_fb_set(gfx, y * gfx->framebuffer_width + x, color);
|
||||||
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
|
|
||||||
((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color);
|
|
||||||
} else {
|
|
||||||
gfx->framebuffer[idx] = color & 0xFF;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_rect_fill(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) {
|
void pxl8_rect_fill(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) {
|
||||||
|
|
@ -648,15 +644,10 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) {
|
||||||
i32 dest_idx = (dest_y + py) * gfx->framebuffer_width + (dest_x + px);
|
i32 dest_idx = (dest_y + py) * gfx->framebuffer_width + (dest_x + px);
|
||||||
|
|
||||||
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
|
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
|
||||||
u16 s = ((const u16*)atlas_pixels)[src_idx];
|
u16* fb16 = (u16*)gfx->framebuffer;
|
||||||
u16 d = ((u16*)gfx->framebuffer)[dest_idx];
|
fb16[dest_idx] = pxl8_blend_hicolor(((const u16*)atlas_pixels)[src_idx], fb16[dest_idx]);
|
||||||
u16 m = (u16)(-(s != 0));
|
|
||||||
((u16*)gfx->framebuffer)[dest_idx] = (s & m) | (d & ~m);
|
|
||||||
} else {
|
} else {
|
||||||
u8 s = atlas_pixels[src_idx];
|
gfx->framebuffer[dest_idx] = pxl8_blend_indexed(atlas_pixels[src_idx], gfx->framebuffer[dest_idx]);
|
||||||
u8 d = gfx->framebuffer[dest_idx];
|
|
||||||
u8 m = (u8)(-(s != 0));
|
|
||||||
gfx->framebuffer[dest_idx] = (s & m) | (d & ~m);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,11 @@ typedef struct pxl8_hal {
|
||||||
void (*set_relative_mouse_mode)(void* platform_data, bool enabled);
|
void (*set_relative_mouse_mode)(void* platform_data, bool enabled);
|
||||||
void (*upload_texture)(void* platform_data, const u8* pixels, u32 w, u32 h,
|
void (*upload_texture)(void* platform_data, const u8* pixels, u32 w, u32 h,
|
||||||
const u32* palette, u32 bpp);
|
const u32* palette, u32 bpp);
|
||||||
|
|
||||||
|
void* (*audio_create)(i32 sample_rate, i32 channels);
|
||||||
|
void (*audio_destroy)(void* audio_handle);
|
||||||
|
void (*audio_start)(void* audio_handle);
|
||||||
|
void (*audio_stop)(void* audio_handle);
|
||||||
|
bool (*upload_audio)(void* audio_handle, const f32* stereo_samples, i32 sample_count);
|
||||||
|
i32 (*audio_queued)(void* audio_handle);
|
||||||
} pxl8_hal;
|
} pxl8_hal;
|
||||||
|
|
|
||||||
619
src/pxl8_replay.c
Normal file
619
src/pxl8_replay.c
Normal file
|
|
@ -0,0 +1,619 @@
|
||||||
|
#include "pxl8_replay.h"
|
||||||
|
#include "pxl8_log.h"
|
||||||
|
#include "pxl8_sys.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef struct pxl8_replay_chunk {
|
||||||
|
u8 type;
|
||||||
|
u32 size;
|
||||||
|
u8* data;
|
||||||
|
struct pxl8_replay_chunk* next;
|
||||||
|
} pxl8_replay_chunk;
|
||||||
|
|
||||||
|
typedef struct pxl8_keyframe_entry {
|
||||||
|
pxl8_keyframe keyframe;
|
||||||
|
pxl8_replay_chunk* input_deltas;
|
||||||
|
struct pxl8_keyframe_entry* next;
|
||||||
|
struct pxl8_keyframe_entry* prev;
|
||||||
|
} pxl8_keyframe_entry;
|
||||||
|
|
||||||
|
struct pxl8_replay {
|
||||||
|
FILE* file;
|
||||||
|
pxl8_replay_header header;
|
||||||
|
bool recording;
|
||||||
|
bool playing;
|
||||||
|
u32 current_frame;
|
||||||
|
|
||||||
|
pxl8_keyframe_entry* keyframes;
|
||||||
|
pxl8_keyframe_entry* current_keyframe;
|
||||||
|
u32 keyframe_count;
|
||||||
|
u32 max_keyframes;
|
||||||
|
|
||||||
|
pxl8_replay_chunk* pending_inputs;
|
||||||
|
pxl8_replay_chunk* pending_inputs_tail;
|
||||||
|
|
||||||
|
pxl8_replay_chunk* audio_events;
|
||||||
|
pxl8_replay_chunk* audio_events_tail;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void pxl8_replay_chunk_free(pxl8_replay_chunk* chunk) {
|
||||||
|
while (chunk) {
|
||||||
|
pxl8_replay_chunk* next = chunk->next;
|
||||||
|
free(chunk->data);
|
||||||
|
free(chunk);
|
||||||
|
chunk = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pxl8_replay_keyframe_entry_free(pxl8_keyframe_entry* entry) {
|
||||||
|
while (entry) {
|
||||||
|
pxl8_keyframe_entry* next = entry->next;
|
||||||
|
pxl8_replay_chunk_free(entry->input_deltas);
|
||||||
|
free(entry);
|
||||||
|
entry = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_replay* pxl8_replay_create(const char* path, u32 keyframe_interval) {
|
||||||
|
FILE* f = fopen(path, "wb");
|
||||||
|
if (!f) {
|
||||||
|
pxl8_error("Failed to create replay file: %s", path);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_replay* r = calloc(1, sizeof(pxl8_replay));
|
||||||
|
if (!r) {
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
r->file = f;
|
||||||
|
r->recording = true;
|
||||||
|
r->playing = false;
|
||||||
|
r->header.magic = PXL8_REPLAY_MAGIC;
|
||||||
|
r->header.version = PXL8_REPLAY_VERSION;
|
||||||
|
r->header.keyframe_interval = keyframe_interval;
|
||||||
|
|
||||||
|
fwrite(&r->header, sizeof(pxl8_replay_header), 1, f);
|
||||||
|
fflush(f);
|
||||||
|
|
||||||
|
pxl8_info("Created replay file: %s (keyframe interval: %u)", path, keyframe_interval);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_replay* pxl8_replay_create_buffer(u32 keyframe_interval, u32 max_keyframes) {
|
||||||
|
pxl8_replay* r = calloc(1, sizeof(pxl8_replay));
|
||||||
|
if (!r) return NULL;
|
||||||
|
|
||||||
|
r->recording = true;
|
||||||
|
r->playing = false;
|
||||||
|
r->max_keyframes = max_keyframes;
|
||||||
|
r->header.magic = PXL8_REPLAY_MAGIC;
|
||||||
|
r->header.version = PXL8_REPLAY_VERSION;
|
||||||
|
r->header.keyframe_interval = keyframe_interval;
|
||||||
|
|
||||||
|
pxl8_debug("Created replay buffer (keyframe interval: %u, max: %u)", keyframe_interval, max_keyframes);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_replay* pxl8_replay_open(const char* path) {
|
||||||
|
FILE* f = fopen(path, "rb");
|
||||||
|
if (!f) {
|
||||||
|
pxl8_error("Failed to open replay file: %s", path);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_replay_header header;
|
||||||
|
if (fread(&header, sizeof(header), 1, f) != 1) {
|
||||||
|
pxl8_error("Failed to read replay header");
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.magic != PXL8_REPLAY_MAGIC) {
|
||||||
|
pxl8_error("Invalid replay magic: 0x%08X (expected 0x%08X)", header.magic, PXL8_REPLAY_MAGIC);
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.version > PXL8_REPLAY_VERSION) {
|
||||||
|
pxl8_error("Unsupported replay version: %u (max supported: %u)", header.version, PXL8_REPLAY_VERSION);
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_replay* r = calloc(1, sizeof(pxl8_replay));
|
||||||
|
if (!r) {
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
r->file = f;
|
||||||
|
r->header = header;
|
||||||
|
r->recording = false;
|
||||||
|
r->playing = true;
|
||||||
|
|
||||||
|
while (!feof(f)) {
|
||||||
|
u8 chunk_type;
|
||||||
|
if (fread(&chunk_type, 1, 1, f) != 1) break;
|
||||||
|
|
||||||
|
if (chunk_type == PXL8_REPLAY_CHUNK_END) break;
|
||||||
|
|
||||||
|
u8 size_bytes[3];
|
||||||
|
if (fread(size_bytes, 3, 1, f) != 1) break;
|
||||||
|
u32 size = size_bytes[0] | (size_bytes[1] << 8) | (size_bytes[2] << 16);
|
||||||
|
|
||||||
|
u8* data = malloc(size);
|
||||||
|
if (!data || fread(data, size, 1, f) != 1) {
|
||||||
|
free(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk_type == PXL8_REPLAY_CHUNK_KEYFRAME) {
|
||||||
|
pxl8_keyframe_entry* entry = calloc(1, sizeof(pxl8_keyframe_entry));
|
||||||
|
if (entry && size >= sizeof(pxl8_keyframe)) {
|
||||||
|
memcpy(&entry->keyframe, data, sizeof(pxl8_keyframe));
|
||||||
|
entry->prev = r->current_keyframe;
|
||||||
|
if (r->current_keyframe) {
|
||||||
|
r->current_keyframe->next = entry;
|
||||||
|
}
|
||||||
|
r->current_keyframe = entry;
|
||||||
|
if (!r->keyframes) {
|
||||||
|
r->keyframes = entry;
|
||||||
|
}
|
||||||
|
r->keyframe_count++;
|
||||||
|
}
|
||||||
|
free(data);
|
||||||
|
} else if (chunk_type == PXL8_REPLAY_CHUNK_INPUT) {
|
||||||
|
if (r->current_keyframe) {
|
||||||
|
pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk));
|
||||||
|
if (chunk) {
|
||||||
|
chunk->type = chunk_type;
|
||||||
|
chunk->size = size;
|
||||||
|
chunk->data = data;
|
||||||
|
data = NULL;
|
||||||
|
|
||||||
|
if (!r->current_keyframe->input_deltas) {
|
||||||
|
r->current_keyframe->input_deltas = chunk;
|
||||||
|
} else {
|
||||||
|
pxl8_replay_chunk* tail = r->current_keyframe->input_deltas;
|
||||||
|
while (tail->next) tail = tail->next;
|
||||||
|
tail->next = chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(data);
|
||||||
|
} else if (chunk_type == PXL8_REPLAY_CHUNK_AUDIO_EVENT) {
|
||||||
|
pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk));
|
||||||
|
if (chunk) {
|
||||||
|
chunk->type = chunk_type;
|
||||||
|
chunk->size = size;
|
||||||
|
chunk->data = data;
|
||||||
|
data = NULL;
|
||||||
|
|
||||||
|
if (!r->audio_events) {
|
||||||
|
r->audio_events = chunk;
|
||||||
|
r->audio_events_tail = chunk;
|
||||||
|
} else {
|
||||||
|
r->audio_events_tail->next = chunk;
|
||||||
|
r->audio_events_tail = chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(data);
|
||||||
|
} else {
|
||||||
|
free(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r->current_keyframe = r->keyframes;
|
||||||
|
pxl8_info("Opened replay: %u frames, %u keyframes", r->header.total_frames, r->keyframe_count);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_replay_destroy(pxl8_replay* r) {
|
||||||
|
if (!r) return;
|
||||||
|
|
||||||
|
if (r->file) {
|
||||||
|
if (r->recording) {
|
||||||
|
u8 end_chunk = PXL8_REPLAY_CHUNK_END;
|
||||||
|
fwrite(&end_chunk, 1, 1, r->file);
|
||||||
|
|
||||||
|
fseek(r->file, 0, SEEK_SET);
|
||||||
|
fwrite(&r->header, sizeof(pxl8_replay_header), 1, r->file);
|
||||||
|
}
|
||||||
|
fclose(r->file);
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_replay_keyframe_entry_free(r->keyframes);
|
||||||
|
pxl8_replay_chunk_free(r->pending_inputs);
|
||||||
|
pxl8_replay_chunk_free(r->audio_events);
|
||||||
|
|
||||||
|
free(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pxl8_replay_is_recording(pxl8_replay* r) {
|
||||||
|
return r && r->recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pxl8_replay_is_playing(pxl8_replay* r) {
|
||||||
|
return r && r->playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 pxl8_replay_get_frame(pxl8_replay* r) {
|
||||||
|
return r ? r->current_frame : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 pxl8_replay_get_total_frames(pxl8_replay* r) {
|
||||||
|
return r ? r->header.total_frames : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_chunk(FILE* f, u8 type, const void* data, u32 size) {
|
||||||
|
fwrite(&type, 1, 1, f);
|
||||||
|
u8 size_bytes[3] = {
|
||||||
|
(u8)(size & 0xFF),
|
||||||
|
(u8)((size >> 8) & 0xFF),
|
||||||
|
(u8)((size >> 16) & 0xFF)
|
||||||
|
};
|
||||||
|
fwrite(size_bytes, 3, 1, f);
|
||||||
|
fwrite(data, size, 1, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void add_chunk_to_buffer(pxl8_replay* r, u8 type, const void* data, u32 size) {
|
||||||
|
pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk));
|
||||||
|
if (!chunk) return;
|
||||||
|
|
||||||
|
chunk->type = type;
|
||||||
|
chunk->size = size;
|
||||||
|
chunk->data = malloc(size);
|
||||||
|
if (!chunk->data) {
|
||||||
|
free(chunk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memcpy(chunk->data, data, size);
|
||||||
|
|
||||||
|
if (!r->pending_inputs) {
|
||||||
|
r->pending_inputs = chunk;
|
||||||
|
r->pending_inputs_tail = chunk;
|
||||||
|
} else {
|
||||||
|
r->pending_inputs_tail->next = chunk;
|
||||||
|
r->pending_inputs_tail = chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pack_keys(const bool* keys, u8* packed) {
|
||||||
|
memset(packed, 0, 32);
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
if (keys[i]) {
|
||||||
|
packed[i / 8] |= (1 << (i % 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void unpack_keys(const u8* packed, bool* keys) {
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
keys[i] = (packed[i / 8] >> (i % 8)) & 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static u8 pack_mouse_buttons(const bool* buttons) {
|
||||||
|
return (buttons[0] ? 1 : 0) | (buttons[1] ? 2 : 0) | (buttons[2] ? 4 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void unpack_mouse_buttons(u8 packed, bool* buttons) {
|
||||||
|
buttons[0] = (packed & 1) != 0;
|
||||||
|
buttons[1] = (packed & 2) != 0;
|
||||||
|
buttons[2] = (packed & 4) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_replay_write_keyframe(pxl8_replay* r, u32 frame, f32 time, pxl8_rng* rng, pxl8_input_state* input) {
|
||||||
|
if (!r || !r->recording) return;
|
||||||
|
|
||||||
|
pxl8_keyframe kf = {0};
|
||||||
|
kf.frame_number = frame;
|
||||||
|
kf.time = time;
|
||||||
|
kf.rng_state = rng ? rng->state : 0;
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
pack_keys(input->keys_down, kf.keys_down);
|
||||||
|
kf.mouse_buttons = pack_mouse_buttons(input->mouse_buttons_down);
|
||||||
|
kf.mouse_x = (i16)input->mouse_x;
|
||||||
|
kf.mouse_y = (i16)input->mouse_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r->file) {
|
||||||
|
write_chunk(r->file, PXL8_REPLAY_CHUNK_KEYFRAME, &kf, sizeof(kf));
|
||||||
|
fflush(r->file);
|
||||||
|
} else {
|
||||||
|
pxl8_keyframe_entry* entry = calloc(1, sizeof(pxl8_keyframe_entry));
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
entry->keyframe = kf;
|
||||||
|
entry->input_deltas = r->pending_inputs;
|
||||||
|
r->pending_inputs = NULL;
|
||||||
|
r->pending_inputs_tail = NULL;
|
||||||
|
|
||||||
|
if (r->keyframe_count >= r->max_keyframes && r->keyframes) {
|
||||||
|
pxl8_keyframe_entry* oldest = r->keyframes;
|
||||||
|
r->keyframes = oldest->next;
|
||||||
|
if (r->keyframes) {
|
||||||
|
r->keyframes->prev = NULL;
|
||||||
|
}
|
||||||
|
pxl8_replay_chunk_free(oldest->input_deltas);
|
||||||
|
free(oldest);
|
||||||
|
r->keyframe_count--;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry->prev = r->current_keyframe;
|
||||||
|
if (r->current_keyframe) {
|
||||||
|
r->current_keyframe->next = entry;
|
||||||
|
}
|
||||||
|
r->current_keyframe = entry;
|
||||||
|
if (!r->keyframes) {
|
||||||
|
r->keyframes = entry;
|
||||||
|
}
|
||||||
|
r->keyframe_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
r->current_frame = frame;
|
||||||
|
r->header.total_frames = frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_replay_write_input(pxl8_replay* r, u32 frame, pxl8_input_state* prev, pxl8_input_state* curr) {
|
||||||
|
if (!r || !r->recording || !prev || !curr) return;
|
||||||
|
|
||||||
|
u8 buffer[256];
|
||||||
|
u32 offset = 0;
|
||||||
|
|
||||||
|
buffer[offset++] = (u8)(frame & 0xFF);
|
||||||
|
buffer[offset++] = (u8)((frame >> 8) & 0xFF);
|
||||||
|
buffer[offset++] = (u8)((frame >> 16) & 0xFF);
|
||||||
|
buffer[offset++] = (u8)((frame >> 24) & 0xFF);
|
||||||
|
|
||||||
|
u8 key_event_count = 0;
|
||||||
|
u8 key_events[64];
|
||||||
|
u32 key_offset = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 256 && key_event_count < 32; i++) {
|
||||||
|
if (prev->keys_down[i] != curr->keys_down[i]) {
|
||||||
|
key_events[key_offset++] = (u8)i;
|
||||||
|
key_events[key_offset++] = curr->keys_down[i] ? 1 : 0;
|
||||||
|
key_event_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[offset++] = key_event_count;
|
||||||
|
memcpy(buffer + offset, key_events, key_offset);
|
||||||
|
offset += key_offset;
|
||||||
|
|
||||||
|
u8 prev_mouse_btns = pack_mouse_buttons(prev->mouse_buttons_down);
|
||||||
|
u8 curr_mouse_btns = pack_mouse_buttons(curr->mouse_buttons_down);
|
||||||
|
|
||||||
|
u8 mouse_flags = 0;
|
||||||
|
if (curr->mouse_x != prev->mouse_x || curr->mouse_y != prev->mouse_y) {
|
||||||
|
mouse_flags |= 0x01;
|
||||||
|
}
|
||||||
|
if (curr_mouse_btns != prev_mouse_btns) {
|
||||||
|
mouse_flags |= 0x02;
|
||||||
|
}
|
||||||
|
if (curr->mouse_wheel_x != 0 || curr->mouse_wheel_y != 0) {
|
||||||
|
mouse_flags |= 0x04;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[offset++] = mouse_flags;
|
||||||
|
|
||||||
|
if (mouse_flags & 0x01) {
|
||||||
|
i16 dx = (i16)(curr->mouse_x - prev->mouse_x);
|
||||||
|
i16 dy = (i16)(curr->mouse_y - prev->mouse_y);
|
||||||
|
buffer[offset++] = (u8)(dx & 0xFF);
|
||||||
|
buffer[offset++] = (u8)((dx >> 8) & 0xFF);
|
||||||
|
buffer[offset++] = (u8)(dy & 0xFF);
|
||||||
|
buffer[offset++] = (u8)((dy >> 8) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse_flags & 0x02) {
|
||||||
|
buffer[offset++] = curr_mouse_btns;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse_flags & 0x04) {
|
||||||
|
buffer[offset++] = (i8)curr->mouse_wheel_x;
|
||||||
|
buffer[offset++] = (i8)curr->mouse_wheel_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r->file) {
|
||||||
|
write_chunk(r->file, PXL8_REPLAY_CHUNK_INPUT, buffer, offset);
|
||||||
|
} else {
|
||||||
|
add_chunk_to_buffer(r, PXL8_REPLAY_CHUNK_INPUT, buffer, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
r->current_frame = frame;
|
||||||
|
r->header.total_frames = frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_replay_write_audio_event(pxl8_replay* r, u32 frame, u8 event_type, u8 context_id, u8 note, f32 volume) {
|
||||||
|
if (!r || !r->recording) return;
|
||||||
|
|
||||||
|
pxl8_audio_event evt = {
|
||||||
|
.frame_number = frame,
|
||||||
|
.event_type = event_type,
|
||||||
|
.context_id = context_id,
|
||||||
|
.note = note,
|
||||||
|
.volume = volume
|
||||||
|
};
|
||||||
|
|
||||||
|
if (r->file) {
|
||||||
|
write_chunk(r->file, PXL8_REPLAY_CHUNK_AUDIO_EVENT, &evt, sizeof(evt));
|
||||||
|
} else {
|
||||||
|
pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk));
|
||||||
|
if (!chunk) return;
|
||||||
|
|
||||||
|
chunk->type = PXL8_REPLAY_CHUNK_AUDIO_EVENT;
|
||||||
|
chunk->size = sizeof(evt);
|
||||||
|
chunk->data = malloc(sizeof(evt));
|
||||||
|
if (!chunk->data) {
|
||||||
|
free(chunk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memcpy(chunk->data, &evt, sizeof(evt));
|
||||||
|
|
||||||
|
if (!r->audio_events) {
|
||||||
|
r->audio_events = chunk;
|
||||||
|
r->audio_events_tail = chunk;
|
||||||
|
} else {
|
||||||
|
r->audio_events_tail->next = chunk;
|
||||||
|
r->audio_events_tail = chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_replay_close(pxl8_replay* r) {
|
||||||
|
pxl8_replay_destroy(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pxl8_replay_seek_frame(pxl8_replay* r, u32 frame) {
|
||||||
|
if (!r || !r->playing) return false;
|
||||||
|
|
||||||
|
pxl8_keyframe_entry* target = NULL;
|
||||||
|
for (pxl8_keyframe_entry* e = r->keyframes; e; e = e->next) {
|
||||||
|
if (e->keyframe.frame_number <= frame) {
|
||||||
|
target = e;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) return false;
|
||||||
|
|
||||||
|
r->current_keyframe = target;
|
||||||
|
r->current_frame = target->keyframe.frame_number;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pxl8_replay_read_frame(pxl8_replay* r, pxl8_input_state* input_out) {
|
||||||
|
if (!r || !r->playing || !r->current_keyframe) return false;
|
||||||
|
|
||||||
|
u32 target_frame = r->current_frame + 1;
|
||||||
|
|
||||||
|
if (target_frame > r->header.total_frames) return false;
|
||||||
|
|
||||||
|
pxl8_replay_chunk* delta = r->current_keyframe->input_deltas;
|
||||||
|
while (delta) {
|
||||||
|
if (delta->size >= 5) {
|
||||||
|
u32 delta_frame = delta->data[0] | (delta->data[1] << 8) | (delta->data[2] << 16) | (delta->data[3] << 24);
|
||||||
|
if (delta_frame == target_frame) {
|
||||||
|
u32 offset = 4;
|
||||||
|
u8 key_event_count = delta->data[offset++];
|
||||||
|
|
||||||
|
for (u8 i = 0; i < key_event_count && offset + 1 < delta->size; i++) {
|
||||||
|
u8 scancode = delta->data[offset++];
|
||||||
|
u8 pressed = delta->data[offset++];
|
||||||
|
input_out->keys_down[scancode] = pressed != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset < delta->size) {
|
||||||
|
u8 mouse_flags = delta->data[offset++];
|
||||||
|
|
||||||
|
if (mouse_flags & 0x01 && offset + 3 < delta->size) {
|
||||||
|
i16 dx = (i16)(delta->data[offset] | (delta->data[offset + 1] << 8));
|
||||||
|
i16 dy = (i16)(delta->data[offset + 2] | (delta->data[offset + 3] << 8));
|
||||||
|
offset += 4;
|
||||||
|
input_out->mouse_x += dx;
|
||||||
|
input_out->mouse_y += dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse_flags & 0x02 && offset < delta->size) {
|
||||||
|
u8 mouse_btns = delta->data[offset++];
|
||||||
|
unpack_mouse_buttons(mouse_btns, input_out->mouse_buttons_down);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse_flags & 0x04 && offset + 1 < delta->size) {
|
||||||
|
input_out->mouse_wheel_x = (i8)delta->data[offset++];
|
||||||
|
input_out->mouse_wheel_y = (i8)delta->data[offset++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r->current_frame = target_frame;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delta = delta->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r->current_keyframe->next && r->current_keyframe->next->keyframe.frame_number <= target_frame) {
|
||||||
|
r->current_keyframe = r->current_keyframe->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
r->current_frame = target_frame;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pxl8_replay_get_keyframe(pxl8_replay* r, u32 frame, pxl8_keyframe* out) {
|
||||||
|
if (!r || !out) return false;
|
||||||
|
|
||||||
|
pxl8_keyframe_entry* target = NULL;
|
||||||
|
for (pxl8_keyframe_entry* e = r->keyframes; e; e = e->next) {
|
||||||
|
if (e->keyframe.frame_number <= frame) {
|
||||||
|
target = e;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) return false;
|
||||||
|
|
||||||
|
*out = target->keyframe;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_replay_apply_keyframe(pxl8_replay* r, pxl8_keyframe* kf, pxl8_rng* rng, pxl8_input_state* input) {
|
||||||
|
if (!kf) return;
|
||||||
|
|
||||||
|
if (rng) {
|
||||||
|
rng->state = kf->rng_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
unpack_keys(kf->keys_down, input->keys_down);
|
||||||
|
unpack_mouse_buttons(kf->mouse_buttons, input->mouse_buttons_down);
|
||||||
|
input->mouse_x = kf->mouse_x;
|
||||||
|
input->mouse_y = kf->mouse_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r) {
|
||||||
|
r->current_frame = kf->frame_number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pxl8_replay_export(pxl8_replay* r, const char* path) {
|
||||||
|
if (!r || !r->keyframes) return false;
|
||||||
|
|
||||||
|
FILE* f = fopen(path, "wb");
|
||||||
|
if (!f) {
|
||||||
|
pxl8_error("Failed to create export file: %s", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_replay_header header = r->header;
|
||||||
|
fwrite(&header, sizeof(header), 1, f);
|
||||||
|
|
||||||
|
for (pxl8_keyframe_entry* e = r->keyframes; e; e = e->next) {
|
||||||
|
write_chunk(f, PXL8_REPLAY_CHUNK_KEYFRAME, &e->keyframe, sizeof(pxl8_keyframe));
|
||||||
|
|
||||||
|
for (pxl8_replay_chunk* c = e->input_deltas; c; c = c->next) {
|
||||||
|
write_chunk(f, c->type, c->data, c->size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pxl8_replay_chunk* c = r->audio_events; c; c = c->next) {
|
||||||
|
write_chunk(f, c->type, c->data, c->size);
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 end_chunk = PXL8_REPLAY_CHUNK_END;
|
||||||
|
fwrite(&end_chunk, 1, 1, f);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
pxl8_info("Exported replay to: %s (%u frames)", path, header.total_frames);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
84
src/pxl8_replay.h
Normal file
84
src/pxl8_replay.h
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "pxl8_io.h"
|
||||||
|
#include "pxl8_rng.h"
|
||||||
|
#include "pxl8_types.h"
|
||||||
|
|
||||||
|
#define PXL8_REPLAY_MAGIC 0x31525850
|
||||||
|
#define PXL8_REPLAY_VERSION 1
|
||||||
|
|
||||||
|
#define PXL8_REPLAY_CHUNK_KEYFRAME 0x01
|
||||||
|
#define PXL8_REPLAY_CHUNK_INPUT 0x02
|
||||||
|
#define PXL8_REPLAY_CHUNK_AUDIO_EVENT 0x03
|
||||||
|
#define PXL8_REPLAY_CHUNK_END 0xFF
|
||||||
|
|
||||||
|
#define PXL8_REPLAY_FLAG_HAS_PALETTE (1 << 0)
|
||||||
|
#define PXL8_REPLAY_FLAG_HAS_GLOBALS (1 << 1)
|
||||||
|
|
||||||
|
typedef struct pxl8_replay pxl8_replay;
|
||||||
|
|
||||||
|
typedef struct pxl8_replay_header {
|
||||||
|
u32 magic;
|
||||||
|
u32 version;
|
||||||
|
u32 flags;
|
||||||
|
u32 keyframe_interval;
|
||||||
|
u32 total_frames;
|
||||||
|
u32 width;
|
||||||
|
u32 height;
|
||||||
|
u32 reserved;
|
||||||
|
} pxl8_replay_header;
|
||||||
|
|
||||||
|
typedef struct pxl8_keyframe {
|
||||||
|
u32 frame_number;
|
||||||
|
f32 time;
|
||||||
|
u32 rng_state;
|
||||||
|
u8 keys_down[32];
|
||||||
|
u8 mouse_buttons;
|
||||||
|
i16 mouse_x;
|
||||||
|
i16 mouse_y;
|
||||||
|
u8 flags;
|
||||||
|
} pxl8_keyframe;
|
||||||
|
|
||||||
|
typedef struct pxl8_input_delta {
|
||||||
|
u32 frame_number;
|
||||||
|
u8 key_event_count;
|
||||||
|
u8 mouse_flags;
|
||||||
|
} pxl8_input_delta;
|
||||||
|
|
||||||
|
typedef struct pxl8_audio_event {
|
||||||
|
u32 frame_number;
|
||||||
|
u8 event_type;
|
||||||
|
u8 context_id;
|
||||||
|
u8 note;
|
||||||
|
f32 volume;
|
||||||
|
} pxl8_audio_event;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
pxl8_replay* pxl8_replay_create(const char* path, u32 keyframe_interval);
|
||||||
|
pxl8_replay* pxl8_replay_create_buffer(u32 keyframe_interval, u32 max_keyframes);
|
||||||
|
pxl8_replay* pxl8_replay_open(const char* path);
|
||||||
|
void pxl8_replay_destroy(pxl8_replay* r);
|
||||||
|
|
||||||
|
bool pxl8_replay_is_recording(pxl8_replay* r);
|
||||||
|
bool pxl8_replay_is_playing(pxl8_replay* r);
|
||||||
|
u32 pxl8_replay_get_frame(pxl8_replay* r);
|
||||||
|
u32 pxl8_replay_get_total_frames(pxl8_replay* r);
|
||||||
|
|
||||||
|
void pxl8_replay_write_keyframe(pxl8_replay* r, u32 frame, f32 time, pxl8_rng* rng, pxl8_input_state* input);
|
||||||
|
void pxl8_replay_write_input(pxl8_replay* r, u32 frame, pxl8_input_state* prev, pxl8_input_state* curr);
|
||||||
|
void pxl8_replay_write_audio_event(pxl8_replay* r, u32 frame, u8 event_type, u8 context_id, u8 note, f32 volume);
|
||||||
|
void pxl8_replay_close(pxl8_replay* r);
|
||||||
|
|
||||||
|
bool pxl8_replay_seek_frame(pxl8_replay* r, u32 frame);
|
||||||
|
bool pxl8_replay_read_frame(pxl8_replay* r, pxl8_input_state* input_out);
|
||||||
|
bool pxl8_replay_get_keyframe(pxl8_replay* r, u32 frame, pxl8_keyframe* out);
|
||||||
|
void pxl8_replay_apply_keyframe(pxl8_replay* r, pxl8_keyframe* kf, pxl8_rng* rng, pxl8_input_state* input);
|
||||||
|
|
||||||
|
bool pxl8_replay_export(pxl8_replay* r, const char* path);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
24
src/pxl8_rng.c
Normal file
24
src/pxl8_rng.c
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include "pxl8_rng.h"
|
||||||
|
|
||||||
|
void pxl8_rng_seed(pxl8_rng* rng, u32 seed) {
|
||||||
|
if (!rng) return;
|
||||||
|
rng->state = seed ? seed : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 pxl8_rng_next(pxl8_rng* rng) {
|
||||||
|
if (!rng) return 0;
|
||||||
|
rng->state ^= rng->state << 13;
|
||||||
|
rng->state ^= rng->state >> 17;
|
||||||
|
rng->state ^= rng->state << 5;
|
||||||
|
return rng->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
f32 pxl8_rng_f32(pxl8_rng* rng) {
|
||||||
|
return (f32)pxl8_rng_next(rng) / (f32)0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
i32 pxl8_rng_range(pxl8_rng* rng, i32 min, i32 max) {
|
||||||
|
if (min >= max) return min;
|
||||||
|
u32 range = (u32)(max - min);
|
||||||
|
return min + (i32)(pxl8_rng_next(rng) % range);
|
||||||
|
}
|
||||||
20
src/pxl8_rng.h
Normal file
20
src/pxl8_rng.h
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "pxl8_types.h"
|
||||||
|
|
||||||
|
typedef struct pxl8_rng {
|
||||||
|
u32 state;
|
||||||
|
} pxl8_rng;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void pxl8_rng_seed(pxl8_rng* rng, u32 seed);
|
||||||
|
u32 pxl8_rng_next(pxl8_rng* rng);
|
||||||
|
f32 pxl8_rng_f32(pxl8_rng* rng);
|
||||||
|
i32 pxl8_rng_range(pxl8_rng* rng, i32 min, i32 max);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -169,6 +169,12 @@ static const char* pxl8_ffi_cdefs =
|
||||||
"typedef struct pxl8_gfx pxl8_gfx;\n"
|
"typedef struct pxl8_gfx pxl8_gfx;\n"
|
||||||
"typedef struct { int x, y, w, h; } pxl8_bounds;\n"
|
"typedef struct { int x, y, w, h; } pxl8_bounds;\n"
|
||||||
"typedef struct { int x, y; } pxl8_point;\n"
|
"typedef struct { int x, y; } pxl8_point;\n"
|
||||||
|
"typedef struct pxl8_rng { u32 state; } pxl8_rng;\n"
|
||||||
|
"\n"
|
||||||
|
"void pxl8_rng_seed(pxl8_rng* rng, u32 seed);\n"
|
||||||
|
"u32 pxl8_rng_next(pxl8_rng* rng);\n"
|
||||||
|
"f32 pxl8_rng_f32(pxl8_rng* rng);\n"
|
||||||
|
"i32 pxl8_rng_range(pxl8_rng* rng, i32 min, i32 max);\n"
|
||||||
"\n"
|
"\n"
|
||||||
"f32 pxl8_get_fps(const pxl8* sys);\n"
|
"f32 pxl8_get_fps(const pxl8* sys);\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
@ -266,7 +272,7 @@ static const char* pxl8_ffi_cdefs =
|
||||||
"} pxl8_raster_bar;\n"
|
"} pxl8_raster_bar;\n"
|
||||||
"\n"
|
"\n"
|
||||||
"typedef struct pxl8_particles pxl8_particles;\n"
|
"typedef struct pxl8_particles pxl8_particles;\n"
|
||||||
"pxl8_particles* pxl8_particles_create(u32 max_count);\n"
|
"pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng);\n"
|
||||||
"void pxl8_particles_destroy(pxl8_particles* particles);\n"
|
"void pxl8_particles_destroy(pxl8_particles* particles);\n"
|
||||||
"void pxl8_particles_clear(pxl8_particles* particles);\n"
|
"void pxl8_particles_clear(pxl8_particles* particles);\n"
|
||||||
"void pxl8_particles_emit(pxl8_particles* particles, u32 count);\n"
|
"void pxl8_particles_emit(pxl8_particles* particles, u32 count);\n"
|
||||||
|
|
@ -460,7 +466,7 @@ static const char* pxl8_ffi_cdefs =
|
||||||
"void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume);\n"
|
"void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume);\n"
|
||||||
"void pxl8_sfx_node_destroy(pxl8_sfx_node* node);\n"
|
"void pxl8_sfx_node_destroy(pxl8_sfx_node* node);\n"
|
||||||
"f32 pxl8_sfx_note_to_freq(u8 note);\n"
|
"f32 pxl8_sfx_note_to_freq(u8 note);\n"
|
||||||
"u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume);\n"
|
"u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume, f32 duration);\n"
|
||||||
"void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id);\n"
|
"void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id);\n"
|
||||||
"pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg);\n"
|
"pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg);\n"
|
||||||
"void pxl8_sfx_reverb_set_damping(pxl8_sfx_node* node, f32 damping);\n"
|
"void pxl8_sfx_reverb_set_damping(pxl8_sfx_node* node, f32 damping);\n"
|
||||||
|
|
@ -684,6 +690,14 @@ void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void pxl8_script_set_rng(pxl8_script* script, void* rng) {
|
||||||
|
if (!script) return;
|
||||||
|
if (script->L && rng) {
|
||||||
|
lua_pushlightuserdata(script->L, rng);
|
||||||
|
lua_setglobal(script->L, "_pxl8_rng");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer) {
|
void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer) {
|
||||||
if (!script) return;
|
if (!script) return;
|
||||||
script->mixer = mixer;
|
script->mixer = mixer;
|
||||||
|
|
@ -747,6 +761,53 @@ pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static time_t get_file_mod_time(const char* path) {
|
||||||
|
struct stat file_stat;
|
||||||
|
if (stat(path, &file_stat) == 0) {
|
||||||
|
return file_stat.st_mtime;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static time_t get_latest_script_mod_time(const char* dir_path) {
|
||||||
|
DIR* dir = opendir(dir_path);
|
||||||
|
if (!dir) return 0;
|
||||||
|
|
||||||
|
time_t latest = 0;
|
||||||
|
struct dirent* entry;
|
||||||
|
|
||||||
|
while ((entry = readdir(dir)) != NULL) {
|
||||||
|
if (entry->d_name[0] == '.') continue;
|
||||||
|
|
||||||
|
char full_path[512];
|
||||||
|
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(full_path, &st) == 0) {
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
time_t subdir_time = get_latest_script_mod_time(full_path);
|
||||||
|
if (subdir_time > latest) {
|
||||||
|
latest = subdir_time;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
size_t len = strlen(entry->d_name);
|
||||||
|
bool is_script = (len > 4 && strcmp(entry->d_name + len - 4, ".fnl") == 0) ||
|
||||||
|
(len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0);
|
||||||
|
|
||||||
|
if (is_script) {
|
||||||
|
time_t mod_time = get_file_mod_time(full_path);
|
||||||
|
if (mod_time > latest) {
|
||||||
|
latest = mod_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd) {
|
void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd) {
|
||||||
if (!script || !script->L || !cart_path || !original_cwd) return;
|
if (!script || !script->L || !cart_path || !original_cwd) return;
|
||||||
|
|
||||||
|
|
@ -779,6 +840,9 @@ void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const
|
||||||
lua_pop(script->L, 1);
|
lua_pop(script->L, 1);
|
||||||
}
|
}
|
||||||
lua_pop(script->L, 1);
|
lua_pop(script->L, 1);
|
||||||
|
|
||||||
|
pxl8_strncpy(script->watch_dir, cart_path, sizeof(script->watch_dir));
|
||||||
|
script->latest_mod_time = get_latest_script_mod_time(script->watch_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) {
|
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) {
|
||||||
|
|
@ -985,7 +1049,7 @@ static bool pxl8_script_is_builtin_global(const char* name) {
|
||||||
"print", "pxl8", "rawequal", "rawget", "rawset", "require",
|
"print", "pxl8", "rawequal", "rawget", "rawset", "require",
|
||||||
"select", "setfenv", "setmetatable", "string", "table",
|
"select", "setfenv", "setmetatable", "string", "table",
|
||||||
"tonumber", "tostring", "type", "unpack", "update", "frame",
|
"tonumber", "tostring", "type", "unpack", "update", "frame",
|
||||||
"xpcall", "_pxl8_gfx", "_pxl8_input", "_pxl8_sfx_mixer", "_pxl8_sys",
|
"xpcall", "_pxl8_gfx", "_pxl8_input", "_pxl8_rng", "_pxl8_sfx_mixer", "_pxl8_sys",
|
||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
for (int i = 0; builtins[i]; i++) {
|
for (int i = 0; builtins[i]; i++) {
|
||||||
|
|
@ -1004,6 +1068,57 @@ static void pxl8_script_cleanup(pxl8_script* script) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void pxl8_script_save_upvalues(lua_State* L, const char* func_name, int saved_table) {
|
||||||
|
lua_getglobal(L, func_name);
|
||||||
|
if (!lua_isfunction(L, -1)) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int func_idx = lua_gettop(L);
|
||||||
|
for (int i = 1; ; i++) {
|
||||||
|
const char* name = lua_getupvalue(L, func_idx, i);
|
||||||
|
if (!name) break;
|
||||||
|
|
||||||
|
int vtype = lua_type(L, -1);
|
||||||
|
if (name[0] != '(' &&
|
||||||
|
vtype != LUA_TFUNCTION &&
|
||||||
|
vtype != LUA_TUSERDATA &&
|
||||||
|
vtype != LUA_TLIGHTUSERDATA &&
|
||||||
|
vtype != LUA_TTHREAD &&
|
||||||
|
vtype != LUA_TTABLE) {
|
||||||
|
lua_pushstring(L, name);
|
||||||
|
lua_pushvalue(L, -2);
|
||||||
|
lua_settable(L, saved_table);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pxl8_script_restore_upvalues(lua_State* L, const char* func_name, int saved_table) {
|
||||||
|
lua_getglobal(L, func_name);
|
||||||
|
if (!lua_isfunction(L, -1)) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int func_idx = lua_gettop(L);
|
||||||
|
for (int i = 1; ; i++) {
|
||||||
|
const char* name = lua_getupvalue(L, func_idx, i);
|
||||||
|
if (!name) break;
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
lua_getfield(L, saved_table, name);
|
||||||
|
if (!lua_isnil(L, -1)) {
|
||||||
|
lua_setupvalue(L, func_idx, i);
|
||||||
|
} else {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
static void pxl8_script_save_globals(pxl8_script* script) {
|
static void pxl8_script_save_globals(pxl8_script* script) {
|
||||||
lua_State* L = script->L;
|
lua_State* L = script->L;
|
||||||
|
|
||||||
|
|
@ -1032,6 +1147,9 @@ static void pxl8_script_save_globals(pxl8_script* script) {
|
||||||
}
|
}
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
pxl8_script_save_upvalues(L, "update", saved_table);
|
||||||
|
pxl8_script_save_upvalues(L, "frame", saved_table);
|
||||||
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state");
|
lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state");
|
||||||
pxl8_debug("Hot reload state saved");
|
pxl8_debug("Hot reload state saved");
|
||||||
}
|
}
|
||||||
|
|
@ -1045,13 +1163,19 @@ static void pxl8_script_restore_globals(pxl8_script* script) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int saved_table = lua_gettop(L);
|
||||||
|
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
while (lua_next(L, -2) != 0) {
|
while (lua_next(L, saved_table) != 0) {
|
||||||
lua_pushvalue(L, -2);
|
lua_pushvalue(L, -2);
|
||||||
lua_pushvalue(L, -2);
|
lua_pushvalue(L, -2);
|
||||||
lua_settable(L, LUA_GLOBALSINDEX);
|
lua_settable(L, LUA_GLOBALSINDEX);
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pxl8_script_restore_upvalues(L, "update", saved_table);
|
||||||
|
pxl8_script_restore_upvalues(L, "frame", saved_table);
|
||||||
|
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
|
|
@ -1059,51 +1183,224 @@ static void pxl8_script_restore_globals(pxl8_script* script) {
|
||||||
pxl8_debug("Hot reload state restored");
|
pxl8_debug("Hot reload state restored");
|
||||||
}
|
}
|
||||||
|
|
||||||
static time_t get_file_mod_time(const char* path) {
|
#define SER_NIL 0
|
||||||
struct stat file_stat;
|
#define SER_BOOL 1
|
||||||
if (stat(path, &file_stat) == 0) {
|
#define SER_NUMBER 2
|
||||||
return file_stat.st_mtime;
|
#define SER_STRING 3
|
||||||
}
|
#define SER_TABLE 4
|
||||||
return 0;
|
|
||||||
|
typedef struct {
|
||||||
|
u8* data;
|
||||||
|
u32 size;
|
||||||
|
u32 capacity;
|
||||||
|
} ser_buffer;
|
||||||
|
|
||||||
|
static void ser_buffer_init(ser_buffer* buf) {
|
||||||
|
buf->capacity = 1024;
|
||||||
|
buf->data = malloc(buf->capacity);
|
||||||
|
buf->size = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static time_t get_latest_script_mod_time(const char* dir_path) {
|
static void ser_buffer_grow(ser_buffer* buf, u32 needed) {
|
||||||
DIR* dir = opendir(dir_path);
|
if (buf->size + needed > buf->capacity) {
|
||||||
if (!dir) return 0;
|
while (buf->size + needed > buf->capacity) {
|
||||||
|
buf->capacity *= 2;
|
||||||
time_t latest = 0;
|
|
||||||
struct dirent* entry;
|
|
||||||
|
|
||||||
while ((entry = readdir(dir)) != NULL) {
|
|
||||||
if (entry->d_name[0] == '.') continue;
|
|
||||||
|
|
||||||
char full_path[512];
|
|
||||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
|
||||||
|
|
||||||
struct stat st;
|
|
||||||
if (stat(full_path, &st) == 0) {
|
|
||||||
if (S_ISDIR(st.st_mode)) {
|
|
||||||
time_t subdir_time = get_latest_script_mod_time(full_path);
|
|
||||||
if (subdir_time > latest) {
|
|
||||||
latest = subdir_time;
|
|
||||||
}
|
}
|
||||||
|
buf->data = realloc(buf->data, buf->capacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ser_write_u8(ser_buffer* buf, u8 v) {
|
||||||
|
ser_buffer_grow(buf, 1);
|
||||||
|
buf->data[buf->size++] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ser_write_u32(ser_buffer* buf, u32 v) {
|
||||||
|
ser_buffer_grow(buf, 4);
|
||||||
|
buf->data[buf->size++] = v & 0xFF;
|
||||||
|
buf->data[buf->size++] = (v >> 8) & 0xFF;
|
||||||
|
buf->data[buf->size++] = (v >> 16) & 0xFF;
|
||||||
|
buf->data[buf->size++] = (v >> 24) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ser_write_f64(ser_buffer* buf, f64 v) {
|
||||||
|
ser_buffer_grow(buf, 8);
|
||||||
|
memcpy(buf->data + buf->size, &v, 8);
|
||||||
|
buf->size += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ser_write_bytes(ser_buffer* buf, const void* data, u32 len) {
|
||||||
|
ser_buffer_grow(buf, len);
|
||||||
|
memcpy(buf->data + buf->size, data, len);
|
||||||
|
buf->size += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ser_write_value(ser_buffer* buf, lua_State* L, int idx, int depth) {
|
||||||
|
int t = lua_type(L, idx);
|
||||||
|
switch (t) {
|
||||||
|
case LUA_TNIL:
|
||||||
|
ser_write_u8(buf, SER_NIL);
|
||||||
|
break;
|
||||||
|
case LUA_TBOOLEAN:
|
||||||
|
ser_write_u8(buf, SER_BOOL);
|
||||||
|
ser_write_u8(buf, lua_toboolean(L, idx) ? 1 : 0);
|
||||||
|
break;
|
||||||
|
case LUA_TNUMBER:
|
||||||
|
ser_write_u8(buf, SER_NUMBER);
|
||||||
|
ser_write_f64(buf, lua_tonumber(L, idx));
|
||||||
|
break;
|
||||||
|
case LUA_TSTRING: {
|
||||||
|
size_t len;
|
||||||
|
const char* str = lua_tolstring(L, idx, &len);
|
||||||
|
ser_write_u8(buf, SER_STRING);
|
||||||
|
ser_write_u32(buf, (u32)len);
|
||||||
|
ser_write_bytes(buf, str, (u32)len);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LUA_TTABLE: {
|
||||||
|
if (depth > 16) { ser_write_u8(buf, SER_NIL); break; }
|
||||||
|
ser_write_u8(buf, SER_TABLE);
|
||||||
|
lua_pushvalue(L, idx);
|
||||||
|
int tbl = lua_gettop(L);
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, tbl) != 0) {
|
||||||
|
ser_write_value(buf, L, -2, depth + 1);
|
||||||
|
ser_write_value(buf, L, -1, depth + 1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
ser_write_u8(buf, SER_NIL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
ser_write_u8(buf, SER_NIL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 pxl8_script_serialize_globals(pxl8_script* script, u8** out_data) {
|
||||||
|
if (!script || !out_data) return 0;
|
||||||
|
lua_State* L = script->L;
|
||||||
|
|
||||||
|
ser_buffer buf;
|
||||||
|
ser_buffer_init(&buf);
|
||||||
|
|
||||||
|
lua_pushvalue(L, LUA_GLOBALSINDEX);
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, -2) != 0) {
|
||||||
|
if (lua_type(L, -2) == LUA_TSTRING) {
|
||||||
|
const char* name = lua_tostring(L, -2);
|
||||||
|
int vtype = lua_type(L, -1);
|
||||||
|
|
||||||
|
if (!pxl8_script_is_builtin_global(name) &&
|
||||||
|
vtype != LUA_TFUNCTION &&
|
||||||
|
vtype != LUA_TUSERDATA &&
|
||||||
|
vtype != LUA_TLIGHTUSERDATA &&
|
||||||
|
vtype != LUA_TTHREAD &&
|
||||||
|
vtype != 10) {
|
||||||
|
ser_write_value(&buf, L, -2, 0);
|
||||||
|
ser_write_value(&buf, L, -1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
ser_write_u8(&buf, SER_NIL);
|
||||||
|
|
||||||
|
*out_data = buf.data;
|
||||||
|
return buf.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const u8* data;
|
||||||
|
u32 size;
|
||||||
|
u32 pos;
|
||||||
|
} deser_buffer;
|
||||||
|
|
||||||
|
static u8 deser_read_u8(deser_buffer* buf) {
|
||||||
|
if (buf->pos >= buf->size) return 0;
|
||||||
|
return buf->data[buf->pos++];
|
||||||
|
}
|
||||||
|
|
||||||
|
static u32 deser_read_u32(deser_buffer* buf) {
|
||||||
|
if (buf->pos + 4 > buf->size) return 0;
|
||||||
|
u32 v = buf->data[buf->pos] | (buf->data[buf->pos+1] << 8) |
|
||||||
|
(buf->data[buf->pos+2] << 16) | (buf->data[buf->pos+3] << 24);
|
||||||
|
buf->pos += 4;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static f64 deser_read_f64(deser_buffer* buf) {
|
||||||
|
if (buf->pos + 8 > buf->size) return 0;
|
||||||
|
f64 v;
|
||||||
|
memcpy(&v, buf->data + buf->pos, 8);
|
||||||
|
buf->pos += 8;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void deser_read_value(deser_buffer* buf, lua_State* L, int depth) {
|
||||||
|
u8 type = deser_read_u8(buf);
|
||||||
|
switch (type) {
|
||||||
|
case SER_NIL:
|
||||||
|
lua_pushnil(L);
|
||||||
|
break;
|
||||||
|
case SER_BOOL:
|
||||||
|
lua_pushboolean(L, deser_read_u8(buf));
|
||||||
|
break;
|
||||||
|
case SER_NUMBER:
|
||||||
|
lua_pushnumber(L, deser_read_f64(buf));
|
||||||
|
break;
|
||||||
|
case SER_STRING: {
|
||||||
|
u32 len = deser_read_u32(buf);
|
||||||
|
if (buf->pos + len <= buf->size) {
|
||||||
|
lua_pushlstring(L, (const char*)(buf->data + buf->pos), len);
|
||||||
|
buf->pos += len;
|
||||||
} else {
|
} else {
|
||||||
size_t len = strlen(entry->d_name);
|
lua_pushnil(L);
|
||||||
bool is_script = (len > 4 && strcmp(entry->d_name + len - 4, ".fnl") == 0) ||
|
}
|
||||||
(len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0);
|
break;
|
||||||
|
}
|
||||||
|
case SER_TABLE: {
|
||||||
|
if (depth > 16) { lua_pushnil(L); break; }
|
||||||
|
lua_newtable(L);
|
||||||
|
while (buf->pos < buf->size) {
|
||||||
|
u8 key_type = deser_read_u8(buf);
|
||||||
|
if (key_type == SER_NIL) break;
|
||||||
|
buf->pos--;
|
||||||
|
deser_read_value(buf, L, depth + 1);
|
||||||
|
deser_read_value(buf, L, depth + 1);
|
||||||
|
lua_settable(L, -3);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
lua_pushnil(L);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (is_script) {
|
void pxl8_script_deserialize_globals(pxl8_script* script, const u8* data, u32 size) {
|
||||||
time_t mod_time = get_file_mod_time(full_path);
|
if (!script || !data || size == 0) return;
|
||||||
if (mod_time > latest) {
|
lua_State* L = script->L;
|
||||||
latest = mod_time;
|
|
||||||
}
|
deser_buffer buf = { .data = data, .size = size, .pos = 0 };
|
||||||
}
|
|
||||||
}
|
while (buf.pos < buf.size) {
|
||||||
}
|
u8 key_type = deser_read_u8(&buf);
|
||||||
|
if (key_type == SER_NIL) break;
|
||||||
|
buf.pos--;
|
||||||
|
|
||||||
|
deser_read_value(&buf, L, 0);
|
||||||
|
deser_read_value(&buf, L, 0);
|
||||||
|
lua_settable(L, LUA_GLOBALSINDEX);
|
||||||
}
|
}
|
||||||
|
|
||||||
closedir(dir);
|
pxl8_debug("Deserialized globals from %u bytes", size);
|
||||||
return latest;
|
}
|
||||||
|
|
||||||
|
void pxl8_script_free_serialized(u8* data) {
|
||||||
|
free(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) {
|
pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) {
|
||||||
|
|
@ -1188,18 +1485,17 @@ bool pxl8_script_check_reload(pxl8_script* script) {
|
||||||
|
|
||||||
if (ext && strcmp(ext, ".fnl") == 0) {
|
if (ext && strcmp(ext, ".fnl") == 0) {
|
||||||
if (pxl8_script_run_fennel_file(script, script->main_path) == PXL8_OK) {
|
if (pxl8_script_run_fennel_file(script, script->main_path) == PXL8_OK) {
|
||||||
pxl8_script_call_function(script, "init");
|
|
||||||
reloaded = true;
|
reloaded = true;
|
||||||
}
|
}
|
||||||
} else if (ext && strcmp(ext, ".lua") == 0) {
|
} else if (ext && strcmp(ext, ".lua") == 0) {
|
||||||
if (pxl8_script_run_file(script, script->main_path) == PXL8_OK) {
|
if (pxl8_script_run_file(script, script->main_path) == PXL8_OK) {
|
||||||
pxl8_script_call_function(script, "init");
|
|
||||||
reloaded = true;
|
reloaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reloaded) {
|
if (reloaded) {
|
||||||
pxl8_script_restore_globals(script);
|
pxl8_script_restore_globals(script);
|
||||||
|
pxl8_script_call_function(script, "init");
|
||||||
}
|
}
|
||||||
|
|
||||||
return reloaded;
|
return reloaded;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ bool pxl8_script_is_incomplete_input(pxl8_script* script);
|
||||||
void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd);
|
void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd);
|
||||||
void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx);
|
void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx);
|
||||||
void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input);
|
void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input);
|
||||||
|
void pxl8_script_set_rng(pxl8_script* script, void* rng);
|
||||||
void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer);
|
void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer);
|
||||||
void pxl8_script_set_sys(pxl8_script* script, void* sys);
|
void pxl8_script_set_sys(pxl8_script* script, void* sys);
|
||||||
|
|
||||||
|
|
@ -34,6 +35,10 @@ pxl8_result pxl8_script_load_module(pxl8_script* script, const char* module_name
|
||||||
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename);
|
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename);
|
||||||
pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename);
|
pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename);
|
||||||
|
|
||||||
|
u32 pxl8_script_serialize_globals(pxl8_script* script, u8** out_data);
|
||||||
|
void pxl8_script_deserialize_globals(pxl8_script* script, const u8* data, u32 size);
|
||||||
|
void pxl8_script_free_serialized(u8* data);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,75 @@ static void sdl3_center_cursor(void* platform_data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct pxl8_sdl3_audio {
|
||||||
|
SDL_AudioStream* stream;
|
||||||
|
i32 sample_rate;
|
||||||
|
i32 channels;
|
||||||
|
} pxl8_sdl3_audio;
|
||||||
|
|
||||||
|
static void* sdl3_audio_create(i32 sample_rate, i32 channels) {
|
||||||
|
pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)SDL_calloc(1, sizeof(pxl8_sdl3_audio));
|
||||||
|
if (!audio) return NULL;
|
||||||
|
|
||||||
|
SDL_AudioSpec spec = {
|
||||||
|
.freq = sample_rate,
|
||||||
|
.channels = channels,
|
||||||
|
.format = SDL_AUDIO_F32
|
||||||
|
};
|
||||||
|
|
||||||
|
audio->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL);
|
||||||
|
if (!audio->stream) {
|
||||||
|
pxl8_error("Failed to open audio device: %s", SDL_GetError());
|
||||||
|
SDL_free(audio);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio->sample_rate = sample_rate;
|
||||||
|
audio->channels = channels;
|
||||||
|
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sdl3_audio_destroy(void* audio_handle) {
|
||||||
|
if (!audio_handle) return;
|
||||||
|
|
||||||
|
pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle;
|
||||||
|
if (audio->stream) {
|
||||||
|
SDL_DestroyAudioStream(audio->stream);
|
||||||
|
}
|
||||||
|
SDL_free(audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sdl3_audio_start(void* audio_handle) {
|
||||||
|
if (!audio_handle) return;
|
||||||
|
|
||||||
|
pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle;
|
||||||
|
SDL_ResumeAudioStreamDevice(audio->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sdl3_audio_stop(void* audio_handle) {
|
||||||
|
if (!audio_handle) return;
|
||||||
|
|
||||||
|
pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle;
|
||||||
|
SDL_PauseAudioStreamDevice(audio->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sdl3_upload_audio(void* audio_handle, const f32* stereo_samples, i32 sample_count) {
|
||||||
|
if (!audio_handle || !stereo_samples) return false;
|
||||||
|
|
||||||
|
pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle;
|
||||||
|
return SDL_PutAudioStreamData(audio->stream, stereo_samples,
|
||||||
|
sample_count * audio->channels * sizeof(f32));
|
||||||
|
}
|
||||||
|
|
||||||
|
static i32 sdl3_audio_queued(void* audio_handle) {
|
||||||
|
if (!audio_handle) return 0;
|
||||||
|
|
||||||
|
pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle;
|
||||||
|
i32 bytes = SDL_GetAudioStreamQueued(audio->stream);
|
||||||
|
return bytes / (audio->channels * sizeof(f32));
|
||||||
|
}
|
||||||
|
|
||||||
const pxl8_hal pxl8_hal_sdl3 = {
|
const pxl8_hal pxl8_hal_sdl3 = {
|
||||||
.create = sdl3_create,
|
.create = sdl3_create,
|
||||||
.destroy = sdl3_destroy,
|
.destroy = sdl3_destroy,
|
||||||
|
|
@ -325,6 +394,12 @@ const pxl8_hal pxl8_hal_sdl3 = {
|
||||||
.center_cursor = sdl3_center_cursor,
|
.center_cursor = sdl3_center_cursor,
|
||||||
.present = sdl3_present,
|
.present = sdl3_present,
|
||||||
.set_cursor = sdl3_set_cursor,
|
.set_cursor = sdl3_set_cursor,
|
||||||
.upload_texture = sdl3_upload_texture,
|
|
||||||
.set_relative_mouse_mode = sdl3_set_relative_mouse_mode,
|
.set_relative_mouse_mode = sdl3_set_relative_mouse_mode,
|
||||||
|
.upload_texture = sdl3_upload_texture,
|
||||||
|
.audio_create = sdl3_audio_create,
|
||||||
|
.audio_destroy = sdl3_audio_destroy,
|
||||||
|
.audio_start = sdl3_audio_start,
|
||||||
|
.audio_stop = sdl3_audio_stop,
|
||||||
|
.upload_audio = sdl3_upload_audio,
|
||||||
|
.audio_queued = sdl3_audio_queued,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
145
src/pxl8_sfx.c
145
src/pxl8_sfx.c
|
|
@ -3,8 +3,7 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include "pxl8_hal.h"
|
||||||
|
|
||||||
#include "pxl8_log.h"
|
#include "pxl8_log.h"
|
||||||
#include "pxl8_math.h"
|
#include "pxl8_math.h"
|
||||||
|
|
||||||
|
|
@ -62,6 +61,8 @@ typedef struct voice {
|
||||||
f32 filter_env_depth;
|
f32 filter_env_depth;
|
||||||
f32 fx_send;
|
f32 fx_send;
|
||||||
f32 start_time;
|
f32 start_time;
|
||||||
|
f32 duration;
|
||||||
|
bool released;
|
||||||
oscillator osc;
|
oscillator osc;
|
||||||
envelope amp_env;
|
envelope amp_env;
|
||||||
envelope filter_env;
|
envelope filter_env;
|
||||||
|
|
@ -125,16 +126,21 @@ struct pxl8_sfx_node {
|
||||||
struct pxl8_sfx_context {
|
struct pxl8_sfx_context {
|
||||||
voice voices[PXL8_SFX_MAX_VOICES];
|
voice voices[PXL8_SFX_MAX_VOICES];
|
||||||
pxl8_sfx_node* fx_head;
|
pxl8_sfx_node* fx_head;
|
||||||
|
pxl8_sfx_mixer* mixer;
|
||||||
f32 volume;
|
f32 volume;
|
||||||
f32 current_time;
|
f32 current_time;
|
||||||
u16 next_voice_id;
|
u16 next_voice_id;
|
||||||
|
u8 context_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct pxl8_sfx_mixer {
|
struct pxl8_sfx_mixer {
|
||||||
SDL_AudioStream* stream;
|
const pxl8_hal* hal;
|
||||||
|
void* audio_handle;
|
||||||
pxl8_sfx_context* contexts[PXL8_SFX_MAX_CONTEXTS];
|
pxl8_sfx_context* contexts[PXL8_SFX_MAX_CONTEXTS];
|
||||||
f32 master_volume;
|
f32 master_volume;
|
||||||
f32* output_buffer;
|
f32* output_buffer;
|
||||||
|
pxl8_sfx_event_callback event_callback;
|
||||||
|
void* event_userdata;
|
||||||
};
|
};
|
||||||
|
|
||||||
static void envelope_trigger(envelope* env) {
|
static void envelope_trigger(envelope* env) {
|
||||||
|
|
@ -334,7 +340,7 @@ static f32 lfo_process(lfo* l) {
|
||||||
return sample * l->depth;
|
return sample * l->depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void voice_trigger(voice* v, u8 note, const pxl8_sfx_voice_params* params, f32 volume, u16 id, f32 time) {
|
static void voice_trigger(voice* v, u8 note, const pxl8_sfx_voice_params* params, f32 volume, u16 id, f32 time, f32 duration) {
|
||||||
v->active = true;
|
v->active = true;
|
||||||
v->id = id;
|
v->id = id;
|
||||||
v->note = note;
|
v->note = note;
|
||||||
|
|
@ -345,6 +351,8 @@ static void voice_trigger(voice* v, u8 note, const pxl8_sfx_voice_params* params
|
||||||
v->filter_env_depth = params->filter_env_depth;
|
v->filter_env_depth = params->filter_env_depth;
|
||||||
v->fx_send = params->fx_send;
|
v->fx_send = params->fx_send;
|
||||||
v->start_time = time;
|
v->start_time = time;
|
||||||
|
v->duration = duration;
|
||||||
|
v->released = false;
|
||||||
|
|
||||||
oscillator_init(&v->osc, params->waveform, v->base_frequency);
|
oscillator_init(&v->osc, params->waveform, v->base_frequency);
|
||||||
v->osc.pulse_width = params->pulse_width;
|
v->osc.pulse_width = params->pulse_width;
|
||||||
|
|
@ -603,10 +611,17 @@ static void context_process_sample(pxl8_sfx_context* ctx, f32* out_left, f32* ou
|
||||||
f32 wet_left = 0.0f, wet_right = 0.0f;
|
f32 wet_left = 0.0f, wet_right = 0.0f;
|
||||||
|
|
||||||
for (int v = 0; v < PXL8_SFX_MAX_VOICES; v++) {
|
for (int v = 0; v < PXL8_SFX_MAX_VOICES; v++) {
|
||||||
if (!ctx->voices[v].active) continue;
|
voice* vp = &ctx->voices[v];
|
||||||
|
if (!vp->active) continue;
|
||||||
|
|
||||||
|
if (vp->duration > 0.0f && !vp->released &&
|
||||||
|
ctx->current_time - vp->start_time >= vp->duration) {
|
||||||
|
voice_release(vp);
|
||||||
|
vp->released = true;
|
||||||
|
}
|
||||||
|
|
||||||
f32 dl, dr, wl, wr;
|
f32 dl, dr, wl, wr;
|
||||||
voice_process(&ctx->voices[v], &dl, &dr, &wl, &wr);
|
voice_process(vp, &dl, &dr, &wl, &wr);
|
||||||
|
|
||||||
dry_left += dl;
|
dry_left += dl;
|
||||||
dry_right += dr;
|
dry_right += dr;
|
||||||
|
|
@ -631,16 +646,58 @@ static void context_process_sample(pxl8_sfx_context* ctx, f32* out_left, f32* ou
|
||||||
*out_right = right;
|
*out_right = right;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SDLCALL audio_callback(void* userdata, SDL_AudioStream* stream, int additional_amount, int total_amount) {
|
pxl8_sfx_mixer* pxl8_sfx_mixer_create(const pxl8_hal* hal) {
|
||||||
(void)total_amount;
|
if (!hal || !hal->audio_create) return NULL;
|
||||||
|
|
||||||
pxl8_sfx_mixer* mixer = (pxl8_sfx_mixer*)userdata;
|
pxl8_sfx_mixer* mixer = (pxl8_sfx_mixer*)calloc(1, sizeof(pxl8_sfx_mixer));
|
||||||
if (!mixer || !mixer->output_buffer) return;
|
if (!mixer) return NULL;
|
||||||
|
|
||||||
u32 samples_needed = additional_amount / (sizeof(f32) * 2);
|
mixer->output_buffer = (f32*)calloc(PXL8_SFX_BUFFER_SIZE * 2, sizeof(f32));
|
||||||
if (samples_needed > PXL8_SFX_BUFFER_SIZE) samples_needed = PXL8_SFX_BUFFER_SIZE;
|
if (!mixer->output_buffer) {
|
||||||
|
free(mixer);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
for (u32 i = 0; i < samples_needed; i++) {
|
mixer->hal = hal;
|
||||||
|
mixer->master_volume = 0.8f;
|
||||||
|
|
||||||
|
mixer->audio_handle = hal->audio_create(PXL8_SFX_SAMPLE_RATE, 2);
|
||||||
|
if (!mixer->audio_handle) {
|
||||||
|
free(mixer->output_buffer);
|
||||||
|
free(mixer);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
hal->audio_start(mixer->audio_handle);
|
||||||
|
pxl8_info("Audio mixer initialized: %d Hz, stereo, %d context slots", PXL8_SFX_SAMPLE_RATE, PXL8_SFX_MAX_CONTEXTS);
|
||||||
|
|
||||||
|
return mixer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_sfx_mixer_destroy(pxl8_sfx_mixer* mixer) {
|
||||||
|
if (!mixer) return;
|
||||||
|
|
||||||
|
if (mixer->hal && mixer->audio_handle) {
|
||||||
|
mixer->hal->audio_destroy(mixer->audio_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(mixer->output_buffer);
|
||||||
|
free(mixer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_sfx_mixer_process(pxl8_sfx_mixer* mixer) {
|
||||||
|
if (!mixer || !mixer->hal || !mixer->audio_handle || !mixer->output_buffer) return;
|
||||||
|
|
||||||
|
i32 queued = mixer->hal->audio_queued(mixer->audio_handle);
|
||||||
|
i32 target = PXL8_SFX_SAMPLE_RATE / 10;
|
||||||
|
|
||||||
|
while (queued < target) {
|
||||||
|
i32 samples_to_generate = PXL8_SFX_BUFFER_SIZE;
|
||||||
|
if (samples_to_generate > target - queued) {
|
||||||
|
samples_to_generate = target - queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i32 i = 0; i < samples_to_generate; i++) {
|
||||||
f32 left = 0.0f, right = 0.0f;
|
f32 left = 0.0f, right = 0.0f;
|
||||||
|
|
||||||
for (int c = 0; c < PXL8_SFX_MAX_CONTEXTS; c++) {
|
for (int c = 0; c < PXL8_SFX_MAX_CONTEXTS; c++) {
|
||||||
|
|
@ -666,50 +723,9 @@ static void SDLCALL audio_callback(void* userdata, SDL_AudioStream* stream, int
|
||||||
mixer->output_buffer[i * 2 + 1] = right;
|
mixer->output_buffer[i * 2 + 1] = right;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_PutAudioStreamData(stream, mixer->output_buffer, samples_needed * sizeof(f32) * 2);
|
mixer->hal->upload_audio(mixer->audio_handle, mixer->output_buffer, samples_to_generate);
|
||||||
}
|
queued += samples_to_generate;
|
||||||
|
|
||||||
pxl8_sfx_mixer* pxl8_sfx_mixer_create(void) {
|
|
||||||
pxl8_sfx_mixer* mixer = (pxl8_sfx_mixer*)calloc(1, sizeof(pxl8_sfx_mixer));
|
|
||||||
if (!mixer) return NULL;
|
|
||||||
|
|
||||||
mixer->output_buffer = (f32*)calloc(PXL8_SFX_BUFFER_SIZE * 2, sizeof(f32));
|
|
||||||
if (!mixer->output_buffer) {
|
|
||||||
free(mixer);
|
|
||||||
return NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mixer->master_volume = 0.8f;
|
|
||||||
|
|
||||||
SDL_AudioSpec spec = {
|
|
||||||
.channels = 2,
|
|
||||||
.format = SDL_AUDIO_F32,
|
|
||||||
.freq = PXL8_SFX_SAMPLE_RATE
|
|
||||||
};
|
|
||||||
|
|
||||||
mixer->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, mixer);
|
|
||||||
if (!mixer->stream) {
|
|
||||||
pxl8_error("Failed to open audio device: %s", SDL_GetError());
|
|
||||||
free(mixer->output_buffer);
|
|
||||||
free(mixer);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_ResumeAudioStreamDevice(mixer->stream);
|
|
||||||
pxl8_info("Audio mixer initialized: %d Hz, stereo, %d context slots", PXL8_SFX_SAMPLE_RATE, PXL8_SFX_MAX_CONTEXTS);
|
|
||||||
|
|
||||||
return mixer;
|
|
||||||
}
|
|
||||||
|
|
||||||
void pxl8_sfx_mixer_destroy(pxl8_sfx_mixer* mixer) {
|
|
||||||
if (!mixer) return;
|
|
||||||
|
|
||||||
if (mixer->stream) {
|
|
||||||
SDL_DestroyAudioStream(mixer->stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
free(mixer->output_buffer);
|
|
||||||
free(mixer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx) {
|
void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx) {
|
||||||
|
|
@ -722,6 +738,8 @@ void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx) {
|
||||||
for (int i = 0; i < PXL8_SFX_MAX_CONTEXTS; i++) {
|
for (int i = 0; i < PXL8_SFX_MAX_CONTEXTS; i++) {
|
||||||
if (!mixer->contexts[i]) {
|
if (!mixer->contexts[i]) {
|
||||||
mixer->contexts[i] = ctx;
|
mixer->contexts[i] = ctx;
|
||||||
|
ctx->mixer = mixer;
|
||||||
|
ctx->context_id = (u8)i;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -755,6 +773,13 @@ void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume) {
|
||||||
if (mixer) mixer->master_volume = fmaxf(0.0f, fminf(1.0f, volume));
|
if (mixer) mixer->master_volume = fmaxf(0.0f, fminf(1.0f, volume));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void pxl8_sfx_mixer_set_event_callback(pxl8_sfx_mixer* mixer, pxl8_sfx_event_callback cb, void* userdata) {
|
||||||
|
if (mixer) {
|
||||||
|
mixer->event_callback = cb;
|
||||||
|
mixer->event_userdata = userdata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
f32 pxl8_sfx_mixer_get_master_volume(const pxl8_sfx_mixer* mixer) {
|
f32 pxl8_sfx_mixer_get_master_volume(const pxl8_sfx_mixer* mixer) {
|
||||||
return mixer ? mixer->master_volume : 0.0f;
|
return mixer ? mixer->master_volume : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
@ -1047,7 +1072,7 @@ static i32 find_free_voice(pxl8_sfx_context* ctx) {
|
||||||
return oldest_active;
|
return oldest_active;
|
||||||
}
|
}
|
||||||
|
|
||||||
u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume) {
|
u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume, f32 duration) {
|
||||||
if (!ctx || !params) return 0;
|
if (!ctx || !params) return 0;
|
||||||
|
|
||||||
i32 slot = find_free_voice(ctx);
|
i32 slot = find_free_voice(ctx);
|
||||||
|
|
@ -1056,7 +1081,11 @@ u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_para
|
||||||
u16 id = ctx->next_voice_id++;
|
u16 id = ctx->next_voice_id++;
|
||||||
if (ctx->next_voice_id == 0) ctx->next_voice_id = 1;
|
if (ctx->next_voice_id == 0) ctx->next_voice_id = 1;
|
||||||
|
|
||||||
voice_trigger(&ctx->voices[slot], note, params, volume, id, ctx->current_time);
|
voice_trigger(&ctx->voices[slot], note, params, volume, id, ctx->current_time, duration);
|
||||||
|
|
||||||
|
if (ctx->mixer && ctx->mixer->event_callback) {
|
||||||
|
ctx->mixer->event_callback(PXL8_SFX_EVENT_NOTE_ON, ctx->context_id, note, volume, ctx->mixer->event_userdata);
|
||||||
|
}
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "pxl8_hal.h"
|
||||||
#include "pxl8_types.h"
|
#include "pxl8_types.h"
|
||||||
|
|
||||||
#define PXL8_SFX_BUFFER_SIZE 1024
|
#define PXL8_SFX_BUFFER_SIZE 1024
|
||||||
|
|
@ -107,17 +108,24 @@ void pxl8_sfx_delay_set_feedback(pxl8_sfx_node* node, f32 feedback);
|
||||||
void pxl8_sfx_delay_set_mix(pxl8_sfx_node* node, f32 mix);
|
void pxl8_sfx_delay_set_mix(pxl8_sfx_node* node, f32 mix);
|
||||||
void pxl8_sfx_delay_set_time(pxl8_sfx_node* node, u32 time_l, u32 time_r);
|
void pxl8_sfx_delay_set_time(pxl8_sfx_node* node, u32 time_l, u32 time_r);
|
||||||
|
|
||||||
|
#define PXL8_SFX_EVENT_NOTE_ON 1
|
||||||
|
#define PXL8_SFX_EVENT_NOTE_OFF 2
|
||||||
|
|
||||||
|
typedef void (*pxl8_sfx_event_callback)(u8 event_type, u8 context_id, u8 note, f32 volume, void* userdata);
|
||||||
|
|
||||||
void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx);
|
void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx);
|
||||||
void pxl8_sfx_mixer_clear(pxl8_sfx_mixer* mixer);
|
void pxl8_sfx_mixer_clear(pxl8_sfx_mixer* mixer);
|
||||||
pxl8_sfx_mixer* pxl8_sfx_mixer_create(void);
|
pxl8_sfx_mixer* pxl8_sfx_mixer_create(const pxl8_hal* hal);
|
||||||
void pxl8_sfx_mixer_destroy(pxl8_sfx_mixer* mixer);
|
void pxl8_sfx_mixer_destroy(pxl8_sfx_mixer* mixer);
|
||||||
void pxl8_sfx_mixer_detach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx);
|
void pxl8_sfx_mixer_detach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx);
|
||||||
f32 pxl8_sfx_mixer_get_master_volume(const pxl8_sfx_mixer* mixer);
|
f32 pxl8_sfx_mixer_get_master_volume(const pxl8_sfx_mixer* mixer);
|
||||||
|
void pxl8_sfx_mixer_process(pxl8_sfx_mixer* mixer);
|
||||||
|
void pxl8_sfx_mixer_set_event_callback(pxl8_sfx_mixer* mixer, pxl8_sfx_event_callback cb, void* userdata);
|
||||||
void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume);
|
void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume);
|
||||||
|
|
||||||
void pxl8_sfx_node_destroy(pxl8_sfx_node* node);
|
void pxl8_sfx_node_destroy(pxl8_sfx_node* node);
|
||||||
f32 pxl8_sfx_note_to_freq(u8 note);
|
f32 pxl8_sfx_note_to_freq(u8 note);
|
||||||
u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume);
|
u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume, f32 duration);
|
||||||
void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id);
|
void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id);
|
||||||
|
|
||||||
pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg);
|
pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg);
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define PXL8_RANDF() ((f32)rand() / (f32)RAND_MAX)
|
#include "pxl8_math.h"
|
||||||
|
|
||||||
struct pxl8_particles {
|
struct pxl8_particles {
|
||||||
pxl8_particle* particles;
|
pxl8_particle* particles;
|
||||||
|
pxl8_rng* rng;
|
||||||
u32 alive_count;
|
u32 alive_count;
|
||||||
u32 count;
|
u32 count;
|
||||||
u32 max_count;
|
u32 max_count;
|
||||||
|
|
@ -22,7 +23,7 @@ struct pxl8_particles {
|
||||||
f32 spawn_timer;
|
f32 spawn_timer;
|
||||||
|
|
||||||
void (*render_fn)(pxl8_gfx* gfx, pxl8_particle* p, void* userdata);
|
void (*render_fn)(pxl8_gfx* gfx, pxl8_particle* p, void* userdata);
|
||||||
void (*spawn_fn)(pxl8_particle* p, void* userdata);
|
void (*spawn_fn)(pxl8_particles* particles, pxl8_particle* p);
|
||||||
void (*update_fn)(pxl8_particle* p, f32 dt, void* userdata);
|
void (*update_fn)(pxl8_particle* p, f32 dt, void* userdata);
|
||||||
void* userdata;
|
void* userdata;
|
||||||
};
|
};
|
||||||
|
|
@ -170,7 +171,7 @@ void pxl8_vfx_water_ripple(pxl8_gfx* gfx, f32* height_map, i32 drop_x, i32 drop_
|
||||||
prev_height = temp;
|
prev_height = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_particles* pxl8_particles_create(u32 max_count) {
|
pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng) {
|
||||||
pxl8_particles* particles = calloc(1, sizeof(pxl8_particles));
|
pxl8_particles* particles = calloc(1, sizeof(pxl8_particles));
|
||||||
if (!particles) return NULL;
|
if (!particles) return NULL;
|
||||||
|
|
||||||
|
|
@ -180,6 +181,7 @@ pxl8_particles* pxl8_particles_create(u32 max_count) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
particles->rng = rng;
|
||||||
particles->max_count = max_count;
|
particles->max_count = max_count;
|
||||||
particles->drag = 0.98f;
|
particles->drag = 0.98f;
|
||||||
particles->gravity_y = 100.0f;
|
particles->gravity_y = 100.0f;
|
||||||
|
|
@ -214,8 +216,8 @@ void pxl8_particles_emit(pxl8_particles* particles, u32 count) {
|
||||||
pxl8_particle* p = &particles->particles[j];
|
pxl8_particle* p = &particles->particles[j];
|
||||||
p->life = 1.0f;
|
p->life = 1.0f;
|
||||||
p->max_life = 1.0f;
|
p->max_life = 1.0f;
|
||||||
p->x = particles->x + ((PXL8_RANDF()) - 0.5f) * particles->spread_x;
|
p->x = particles->x + (pxl8_rng_f32(particles->rng) - 0.5f) * particles->spread_x;
|
||||||
p->y = particles->y + ((PXL8_RANDF()) - 0.5f) * particles->spread_y;
|
p->y = particles->y + (pxl8_rng_f32(particles->rng) - 0.5f) * particles->spread_y;
|
||||||
p->z = 0;
|
p->z = 0;
|
||||||
p->vx = p->vy = p->vz = 0;
|
p->vx = p->vy = p->vz = 0;
|
||||||
p->ax = particles->gravity_x;
|
p->ax = particles->gravity_x;
|
||||||
|
|
@ -228,7 +230,7 @@ void pxl8_particles_emit(pxl8_particles* particles, u32 count) {
|
||||||
p->flags = 1;
|
p->flags = 1;
|
||||||
|
|
||||||
if (particles->spawn_fn) {
|
if (particles->spawn_fn) {
|
||||||
particles->spawn_fn(p, particles->userdata);
|
particles->spawn_fn(particles, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
particles->alive_count++;
|
particles->alive_count++;
|
||||||
|
|
@ -316,28 +318,28 @@ void pxl8_vfx_explosion(pxl8_particles* particles, i32 x, i32 y, u32 color, f32
|
||||||
|
|
||||||
for (u32 i = 0; i < 50 && i < particles->max_count; i++) {
|
for (u32 i = 0; i < 50 && i < particles->max_count; i++) {
|
||||||
pxl8_particle* p = &particles->particles[i];
|
pxl8_particle* p = &particles->particles[i];
|
||||||
f32 angle = (PXL8_RANDF()) * 6.28f;
|
f32 angle = pxl8_rng_f32(particles->rng) * PXL8_TAU;
|
||||||
f32 speed = force * (0.5f + (PXL8_RANDF()) * 0.5f);
|
f32 speed = force * (0.5f + pxl8_rng_f32(particles->rng) * 0.5f);
|
||||||
|
|
||||||
p->x = x;
|
p->x = x;
|
||||||
p->y = y;
|
p->y = y;
|
||||||
p->vx = cosf(angle) * speed;
|
p->vx = cosf(angle) * speed;
|
||||||
p->vy = sinf(angle) * speed;
|
p->vy = sinf(angle) * speed;
|
||||||
p->life = 1.0f;
|
p->life = 1.0f;
|
||||||
p->max_life = 1.0f + (PXL8_RANDF());
|
p->max_life = 1.0f + pxl8_rng_f32(particles->rng);
|
||||||
p->color = color;
|
p->color = color;
|
||||||
p->flags = 1;
|
p->flags = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void fire_spawn(pxl8_particle* p, void* userdata) {
|
static void fire_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||||
uintptr_t palette_start = (uintptr_t)userdata;
|
uintptr_t palette_start = (uintptr_t)particles->userdata;
|
||||||
p->start_color = palette_start + 6 + (rand() % 3);
|
p->start_color = palette_start + 6 + pxl8_rng_range(particles->rng, 0, 3);
|
||||||
p->end_color = palette_start;
|
p->end_color = palette_start;
|
||||||
p->color = p->start_color;
|
p->color = p->start_color;
|
||||||
p->max_life = 1.5f + (PXL8_RANDF()) * 1.5f;
|
p->max_life = 1.5f + pxl8_rng_f32(particles->rng) * 1.5f;
|
||||||
p->vy = -80.0f - (PXL8_RANDF()) * 120.0f;
|
p->vy = -80.0f - pxl8_rng_f32(particles->rng) * 120.0f;
|
||||||
p->vx = ((PXL8_RANDF()) - 0.5f) * 40.0f;
|
p->vx = (pxl8_rng_f32(particles->rng) - 0.5f) * 40.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void fire_update(pxl8_particle* p, f32 dt, void* userdata) {
|
static void fire_update(pxl8_particle* p, f32 dt, void* userdata) {
|
||||||
|
|
@ -376,13 +378,12 @@ void pxl8_vfx_fire(pxl8_particles* particles, i32 x, i32 y, i32 width, u8 palett
|
||||||
particles->userdata = (void*)(uintptr_t)palette_start;
|
particles->userdata = (void*)(uintptr_t)palette_start;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void rain_spawn(pxl8_particle* p, void* userdata) {
|
static void rain_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||||
(void)userdata;
|
p->start_color = 27 + pxl8_rng_range(particles->rng, 0, 3);
|
||||||
p->start_color = 27 + (rand() % 3);
|
|
||||||
p->end_color = 29;
|
p->end_color = 29;
|
||||||
p->color = p->start_color;
|
p->color = p->start_color;
|
||||||
p->max_life = 2.0f;
|
p->max_life = 2.0f;
|
||||||
p->vy = 200.0f + (PXL8_RANDF()) * 100.0f;
|
p->vy = 200.0f + pxl8_rng_f32(particles->rng) * 100.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_vfx_rain(pxl8_particles* particles, i32 width, f32 wind) {
|
void pxl8_vfx_rain(pxl8_particles* particles, i32 width, f32 wind) {
|
||||||
|
|
@ -400,15 +401,15 @@ void pxl8_vfx_rain(pxl8_particles* particles, i32 width, f32 wind) {
|
||||||
particles->update_fn = NULL;
|
particles->update_fn = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void smoke_spawn(pxl8_particle* p, void* userdata) {
|
static void smoke_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||||
uintptr_t base_color = (uintptr_t)userdata;
|
uintptr_t base_color = (uintptr_t)particles->userdata;
|
||||||
p->start_color = base_color;
|
p->start_color = base_color;
|
||||||
p->end_color = base_color + 4;
|
p->end_color = base_color + 4;
|
||||||
p->color = p->start_color;
|
p->color = p->start_color;
|
||||||
p->max_life = 3.0f + (PXL8_RANDF()) * 2.0f;
|
p->max_life = 3.0f + pxl8_rng_f32(particles->rng) * 2.0f;
|
||||||
p->vy = -20.0f - (PXL8_RANDF()) * 30.0f;
|
p->vy = -20.0f - pxl8_rng_f32(particles->rng) * 30.0f;
|
||||||
p->vx = ((PXL8_RANDF()) - 0.5f) * 20.0f;
|
p->vx = (pxl8_rng_f32(particles->rng) - 0.5f) * 20.0f;
|
||||||
p->size = 1.0f + (PXL8_RANDF()) * 2.0f;
|
p->size = 1.0f + pxl8_rng_f32(particles->rng) * 2.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_vfx_smoke(pxl8_particles* particles, i32 x, i32 y, u8 color) {
|
void pxl8_vfx_smoke(pxl8_particles* particles, i32 x, i32 y, u8 color) {
|
||||||
|
|
@ -427,14 +428,13 @@ void pxl8_vfx_smoke(pxl8_particles* particles, i32 x, i32 y, u8 color) {
|
||||||
particles->userdata = (void*)(uintptr_t)color;
|
particles->userdata = (void*)(uintptr_t)color;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void snow_spawn(pxl8_particle* p, void* userdata) {
|
static void snow_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||||
(void)userdata;
|
p->start_color = 8 + pxl8_rng_range(particles->rng, 0, 3);
|
||||||
p->start_color = 8 + (rand() % 3);
|
|
||||||
p->end_color = 10;
|
p->end_color = 10;
|
||||||
p->color = p->start_color;
|
p->color = p->start_color;
|
||||||
p->max_life = 4.0f;
|
p->max_life = 4.0f;
|
||||||
p->vx = ((PXL8_RANDF()) - 0.5f) * 20.0f;
|
p->vx = (pxl8_rng_f32(particles->rng) - 0.5f) * 20.0f;
|
||||||
p->vy = 30.0f + (PXL8_RANDF()) * 20.0f;
|
p->vy = 30.0f + pxl8_rng_f32(particles->rng) * 20.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_vfx_snow(pxl8_particles* particles, i32 width, f32 wind) {
|
void pxl8_vfx_snow(pxl8_particles* particles, i32 width, f32 wind) {
|
||||||
|
|
@ -452,15 +452,15 @@ void pxl8_vfx_snow(pxl8_particles* particles, i32 width, f32 wind) {
|
||||||
particles->update_fn = NULL;
|
particles->update_fn = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void sparks_spawn(pxl8_particle* p, void* userdata) {
|
static void sparks_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||||
uintptr_t base_color = (uintptr_t)userdata;
|
uintptr_t base_color = (uintptr_t)particles->userdata;
|
||||||
p->start_color = base_color;
|
p->start_color = base_color;
|
||||||
p->end_color = base_color > 2 ? base_color - 2 : 0;
|
p->end_color = base_color > 2 ? base_color - 2 : 0;
|
||||||
p->color = p->start_color;
|
p->color = p->start_color;
|
||||||
p->max_life = 0.5f + (PXL8_RANDF()) * 1.0f;
|
p->max_life = 0.5f + pxl8_rng_f32(particles->rng) * 1.0f;
|
||||||
|
|
||||||
f32 angle = (PXL8_RANDF()) * 6.28f;
|
f32 angle = pxl8_rng_f32(particles->rng) * PXL8_TAU;
|
||||||
f32 speed = 100.0f + (PXL8_RANDF()) * 200.0f;
|
f32 speed = 100.0f + pxl8_rng_f32(particles->rng) * 200.0f;
|
||||||
p->vx = cosf(angle) * speed;
|
p->vx = cosf(angle) * speed;
|
||||||
p->vy = sinf(angle) * speed - 50.0f;
|
p->vy = sinf(angle) * speed - 50.0f;
|
||||||
}
|
}
|
||||||
|
|
@ -492,13 +492,13 @@ void pxl8_vfx_starfield(pxl8_particles* particles, f32 speed, f32 spread) {
|
||||||
|
|
||||||
for (u32 i = 0; i < particles->max_count; i++) {
|
for (u32 i = 0; i < particles->max_count; i++) {
|
||||||
pxl8_particle* p = &particles->particles[i];
|
pxl8_particle* p = &particles->particles[i];
|
||||||
p->x = (PXL8_RANDF()) * spread * 2.0f - spread;
|
p->x = pxl8_rng_f32(particles->rng) * spread * 2.0f - spread;
|
||||||
p->y = (PXL8_RANDF()) * spread * 2.0f - spread;
|
p->y = pxl8_rng_f32(particles->rng) * spread * 2.0f - spread;
|
||||||
p->z = (PXL8_RANDF()) * spread;
|
p->z = pxl8_rng_f32(particles->rng) * spread;
|
||||||
p->vz = -speed;
|
p->vz = -speed;
|
||||||
p->life = 1000.0f;
|
p->life = 1000.0f;
|
||||||
p->max_life = 1000.0f;
|
p->max_life = 1000.0f;
|
||||||
p->color = 8 + (rand() % 8);
|
p->color = 8 + pxl8_rng_range(particles->rng, 0, 8);
|
||||||
p->flags = 1;
|
p->flags = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "pxl8_gfx.h"
|
#include "pxl8_gfx.h"
|
||||||
|
#include "pxl8_rng.h"
|
||||||
#include "pxl8_types.h"
|
#include "pxl8_types.h"
|
||||||
|
|
||||||
typedef struct pxl8_particles pxl8_particles;
|
typedef struct pxl8_particles pxl8_particles;
|
||||||
|
|
@ -34,7 +35,7 @@ typedef struct pxl8_raster_bar {
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
pxl8_particles* pxl8_particles_create(u32 max_count);
|
pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng);
|
||||||
void pxl8_particles_destroy(pxl8_particles* particles);
|
void pxl8_particles_destroy(pxl8_particles* particles);
|
||||||
|
|
||||||
void pxl8_particles_clear(pxl8_particles* particles);
|
void pxl8_particles_clear(pxl8_particles* particles);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue