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 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 bass [36 50 40 36 38 38 50 38])
|
||||
(local step-duration 0.15)
|
||||
(local bpm 120)
|
||||
(local beat (/ 60 bpm))
|
||||
(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 []
|
||||
(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 reverb (pxl8.sfx_reverb_create {:room 0.5 :damping 0.5 :mix 0.3}))
|
||||
(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.25 :damping 0.5 :mix 0.5}))
|
||||
(local compressor (pxl8.sfx_compressor_create {:threshold -12 :ratio 4 :attack 10 :release 100}))
|
||||
|
||||
(pxl8.sfx_context_append_node ctx delay)
|
||||
|
|
@ -26,19 +43,20 @@
|
|||
|
||||
(set params (pxl8.sfx_voice_params
|
||||
{: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_cutoff 2500 :filter_resonance 0.15
|
||||
:filter_attack 0.005 :filter_decay 0.1 :filter_sustain 0.1 :filter_release 0.05
|
||||
:filter_cutoff 2500 :filter_resonance 0.05
|
||||
:filter_attack 0.05 :filter_decay 0.1 :filter_sustain 0.2 :filter_release 0.05
|
||||
:filter_env_depth 1500
|
||||
:fx_send 0.3}))
|
||||
:fx_send 0.7})))
|
||||
|
||||
(set bass-params (pxl8.sfx_voice_params
|
||||
{:waveform pxl8.SFX_WAVE_SQUARE
|
||||
:attack 0.005 :decay 0.1 :sustain 0.0 :release 0.05
|
||||
:filter_type pxl8.SFX_FILTER_LOWPASS
|
||||
:filter_cutoff 600 :filter_resonance 0.2
|
||||
:fx_send 0.2})))
|
||||
; (set bass-params (pxl8.sfx_voice_params
|
||||
; {:waveform pxl8.SFX_WAVE_SAW
|
||||
; :attack 0.02 :decay 0.3 :sustain 0.6 :release 0.4
|
||||
; :filter_type pxl8.SFX_FILTER_LOWPASS
|
||||
; :filter_attack 0.02 :filter_decay 0.3 :filter_sustain 0.6 :filter_release 0.4
|
||||
; :filter_cutoff 90 :filter_resonance 0.25
|
||||
; :fx_send 0.2})))
|
||||
|
||||
(fn start []
|
||||
(set playing true)
|
||||
|
|
@ -54,11 +72,16 @@
|
|||
(set time (+ time dt))
|
||||
(when (>= time step-duration)
|
||||
(set time (- time step-duration))
|
||||
(local note (. melody (+ 1 (% step (length melody)))))
|
||||
(pxl8.sfx_play_note ctx note params 0.4)
|
||||
(when (= (% step 4) 0)
|
||||
(local bass-note (. bass (+ 1 (% (math.floor (/ step 4)) (length bass)))))
|
||||
(pxl8.sfx_play_note ctx bass-note bass-params 0.35))
|
||||
(local melody-entry (. melody (+ 1 (% step (length melody)))))
|
||||
(local note (. melody-entry 1))
|
||||
(local dur (. melody-entry 2))
|
||||
(pxl8.sfx_play_note ctx note params 0.4 dur)
|
||||
; (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)))))
|
||||
|
||||
{:init init
|
||||
|
|
|
|||
16
pxl8.sh
16
pxl8.sh
|
|
@ -344,6 +344,8 @@ case "$COMMAND" in
|
|||
src/pxl8_log.c
|
||||
src/pxl8_math.c
|
||||
src/pxl8_repl.c
|
||||
src/pxl8_replay.c
|
||||
src/pxl8_rng.c
|
||||
src/pxl8_save.c
|
||||
src/pxl8_script.c
|
||||
src/pxl8_sdl3.c
|
||||
|
|
@ -380,9 +382,23 @@ case "$COMMAND" in
|
|||
obj_file="$OBJECT_DIR/$obj_name"
|
||||
OBJECTS="$OBJECTS $obj_file"
|
||||
|
||||
NEEDS_REBUILD=false
|
||||
if [[ "$src_file" -nt "$obj_file" ]] || \
|
||||
[[ "src/pxl8_types.h" -nt "$obj_file" ]] || \
|
||||
[[ "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
|
||||
compile_source_file "$src_file" "$obj_file" "$COMPILE_FLAGS"
|
||||
SOURCES_COMPILED="yes"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ local transition = require("pxl8.transition")
|
|||
local anim = require("pxl8.anim")
|
||||
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 = {}
|
||||
|
||||
|
|
@ -29,6 +29,11 @@ pxl8.debug = core.debug
|
|||
pxl8.trace = core.trace
|
||||
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.pixel = gfx2d.pixel
|
||||
pxl8.line = gfx2d.line
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ local C = ffi.C
|
|||
|
||||
local core = {}
|
||||
|
||||
function core.init(gfx, input, sfx_mixer, sys)
|
||||
function core.init(gfx, input, rng, sfx_mixer, sys)
|
||||
core.gfx = gfx
|
||||
core.input = input
|
||||
core.rng = rng
|
||||
core.sfx_mixer = sfx_mixer
|
||||
core.sys = sys
|
||||
end
|
||||
|
|
@ -72,4 +73,20 @@ function core.quit()
|
|||
C.pxl8_set_running(core.sys, false)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ local core = require("pxl8.core")
|
|||
local particles = {}
|
||||
|
||||
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
|
||||
|
||||
function particles.destroy(ps)
|
||||
|
|
|
|||
|
|
@ -124,8 +124,8 @@ function sfx.note_to_freq(note)
|
|||
return C.pxl8_sfx_note_to_freq(note)
|
||||
end
|
||||
|
||||
function sfx.play_note(ctx, note, params, volume)
|
||||
return C.pxl8_sfx_play_note(ctx, note, params, volume or 0.8)
|
||||
function sfx.play_note(ctx, note, params, volume, duration)
|
||||
return C.pxl8_sfx_play_note(ctx, note, params, volume or 0.8, duration or 0)
|
||||
end
|
||||
|
||||
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_macros.h"
|
||||
#include "pxl8_repl.h"
|
||||
#include "pxl8_replay.h"
|
||||
#include "pxl8_script.h"
|
||||
#include "pxl8_sfx.h"
|
||||
#include "pxl8_sys.h"
|
||||
|
||||
struct pxl8 {
|
||||
|
|
@ -27,6 +29,15 @@ struct pxl8 {
|
|||
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* sys = (pxl8*)calloc(1, sizeof(pxl8));
|
||||
if (!sys) return NULL;
|
||||
|
|
@ -218,18 +229,26 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
|
|||
return PXL8_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
|
||||
game->mixer = pxl8_sfx_mixer_create();
|
||||
game->mixer = pxl8_sfx_mixer_create(sys->hal);
|
||||
if (!game->mixer) {
|
||||
pxl8_error("failed to create audio mixer");
|
||||
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) {
|
||||
pxl8_info("starting in REPL mode with script: %s", game->script_path);
|
||||
}
|
||||
|
||||
pxl8_script_set_gfx(game->script, game->gfx);
|
||||
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_sys(game->script, sys);
|
||||
|
||||
|
|
@ -271,7 +290,18 @@ pxl8_result pxl8_update(pxl8* sys) {
|
|||
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->script_loaded) {
|
||||
|
|
@ -309,6 +339,7 @@ pxl8_result pxl8_update(pxl8* sys) {
|
|||
}
|
||||
|
||||
pxl8_gfx_update(game->gfx, dt);
|
||||
pxl8_sfx_mixer_process(game->mixer);
|
||||
|
||||
if (game->script_loaded) {
|
||||
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));
|
||||
|
||||
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_dy = 0;
|
||||
game->input.mouse_wheel_x = 0;
|
||||
|
|
@ -371,6 +414,10 @@ void pxl8_quit(pxl8* sys) {
|
|||
pxl8_cart_unmount(sys->cart);
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
pxl8_replay_destroy(game->debug_replay);
|
||||
#endif
|
||||
|
||||
pxl8_sfx_mixer_destroy(game->mixer);
|
||||
pxl8_gfx_destroy(game->gfx);
|
||||
pxl8_script_destroy(game->script);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ typedef struct pxl8_anim_state_machine {
|
|||
u16 current_state;
|
||||
} 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) {
|
||||
if (!frame_ids || frame_count == 0) {
|
||||
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) {
|
||||
if (!anim) return;
|
||||
|
||||
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) {
|
||||
pxl8_anim_set_frame(state_anim, frame);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (frame < anim->frame_count) {
|
||||
anim->current_frame = frame;
|
||||
anim->time_accumulator = 0.0f;
|
||||
pxl8_anim* target = pxl8_anim_get_active(anim);
|
||||
if (frame < target->frame_count) {
|
||||
target->current_frame = frame;
|
||||
target->time_accumulator = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void pxl8_anim_set_loop(pxl8_anim* anim, bool loop) {
|
||||
if (!anim) return;
|
||||
|
||||
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;
|
||||
pxl8_anim_get_active(anim)->loop = loop;
|
||||
}
|
||||
|
||||
void pxl8_anim_set_reverse(pxl8_anim* anim, bool reverse) {
|
||||
if (!anim) return;
|
||||
|
||||
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;
|
||||
pxl8_anim_get_active(anim)->reverse = reverse;
|
||||
}
|
||||
|
||||
void pxl8_anim_set_speed(pxl8_anim* anim, f32 speed) {
|
||||
if (!anim) return;
|
||||
|
||||
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_anim_get_active(anim)->speed = speed;
|
||||
}
|
||||
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
u16 s0 = (u16)(pixels);
|
||||
u16 s1 = (u16)(pixels >> 16);
|
||||
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);
|
||||
dest_row[col] = pxl8_blend_hicolor((u16)(pixels), dest_row[col]);
|
||||
dest_row[col + 1] = pxl8_blend_hicolor((u16)(pixels >> 16), dest_row[col + 1]);
|
||||
col += 2;
|
||||
}
|
||||
if (w & 1) {
|
||||
u16 s = src_row[col];
|
||||
u16 d = dest_row[col];
|
||||
u16 m = (u16)(-(s != 0));
|
||||
dest_row[col] = (s & m) | (d & ~m);
|
||||
dest_row[col] = pxl8_blend_hicolor(src_row[col], dest_row[col]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,29 +44,14 @@ void pxl8_blit_indexed(u8* fb, u32 fb_width, const u8* sprite, u32 atlas_width,
|
|||
col += 4;
|
||||
continue;
|
||||
}
|
||||
u8 s0 = (u8)(pixels);
|
||||
u8 s1 = (u8)(pixels >> 8);
|
||||
u8 s2 = (u8)(pixels >> 16);
|
||||
u8 s3 = (u8)(pixels >> 24);
|
||||
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);
|
||||
dest_row[col] = pxl8_blend_indexed((u8)(pixels), dest_row[col]);
|
||||
dest_row[col + 1] = pxl8_blend_indexed((u8)(pixels >> 8), dest_row[col + 1]);
|
||||
dest_row[col + 2] = pxl8_blend_indexed((u8)(pixels >> 16), dest_row[col + 2]);
|
||||
dest_row[col + 3] = pxl8_blend_indexed((u8)(pixels >> 24), dest_row[col + 3]);
|
||||
col += 4;
|
||||
}
|
||||
for (; col < w; col++) {
|
||||
u8 s = src_row[col];
|
||||
u8 d = dest_row[col];
|
||||
u8 m = (u8)(-(s != 0));
|
||||
dest_row[col] = (s & m) | (d & ~m);
|
||||
dest_row[col] = pxl8_blend_indexed(src_row[col], dest_row[col]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,16 @@
|
|||
extern "C" {
|
||||
#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(
|
||||
u16* fb, u32 fb_width,
|
||||
const u16* sprite, u32 atlas_width,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include "pxl8_gfx.h"
|
||||
#include "pxl8_rng.h"
|
||||
#include "pxl8_script.h"
|
||||
#include "pxl8_sfx.h"
|
||||
#include "pxl8_types.h"
|
||||
|
||||
typedef struct pxl8_replay pxl8_replay;
|
||||
|
||||
typedef struct pxl8_game {
|
||||
pxl8_gfx* gfx;
|
||||
pxl8_script* script;
|
||||
pxl8_sfx_mixer* mixer;
|
||||
|
||||
pxl8_rng rng;
|
||||
i32 frame_count;
|
||||
u64 last_time;
|
||||
f32 time;
|
||||
|
|
@ -19,6 +23,11 @@ typedef struct pxl8_game {
|
|||
f32 fps;
|
||||
|
||||
pxl8_input_state input;
|
||||
pxl8_input_state prev_input;
|
||||
|
||||
#ifndef NDEBUG
|
||||
pxl8_replay* debug_replay;
|
||||
#endif
|
||||
|
||||
bool repl_mode;
|
||||
bool repl_started;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <string.h>
|
||||
|
||||
#include "pxl8_log.h"
|
||||
#include "pxl8_rng.h"
|
||||
|
||||
typedef struct room_grid {
|
||||
u8* cells;
|
||||
|
|
@ -11,19 +12,6 @@ typedef struct room_grid {
|
|||
i32 height;
|
||||
} 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) {
|
||||
grid->width = width;
|
||||
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->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;
|
||||
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;
|
||||
|
||||
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 h = params->min_room_size + (prng_next() % (params->max_room_size - params->min_room_size + 1));
|
||||
i32 x = 1 + (prng_next() % (params->width - w - 2));
|
||||
i32 y = 1 + (prng_next() % (params->height - h - 2));
|
||||
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 + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1));
|
||||
i32 x = 1 + (pxl8_rng_next(&rng) % (params->width - w - 2));
|
||||
i32 y = 1 + (pxl8_rng_next(&rng) % (params->height - h - 2));
|
||||
|
||||
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_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_v(&grid, prev_cy, new_cy, new_cx);
|
||||
} else {
|
||||
|
|
@ -440,8 +429,6 @@ static u32 hash2d(i32 x, i32 y) {
|
|||
void pxl8_procgen_tex(u8* buffer, const pxl8_procgen_tex_params* params) {
|
||||
if (!buffer || !params) return;
|
||||
|
||||
prng_seed(params->seed);
|
||||
|
||||
for (i32 y = 0; y < params->height; y++) {
|
||||
for (i32 x = 0; x < params->width; x++) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
if (!gfx || !gfx->framebuffer) return;
|
||||
if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return;
|
||||
|
||||
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;
|
||||
}
|
||||
pxl8_fb_set(gfx, y * gfx->framebuffer_width + x, color);
|
||||
}
|
||||
|
||||
u32 pxl8_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) {
|
||||
if (!gfx || !gfx->framebuffer) return 0;
|
||||
if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return 0;
|
||||
|
||||
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];
|
||||
}
|
||||
return pxl8_fb_get(gfx, y * gfx->framebuffer_width + x);
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
pxl8_fb_set(gfx, y * gfx->framebuffer_width + x, 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);
|
||||
|
||||
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
|
||||
u16 s = ((const u16*)atlas_pixels)[src_idx];
|
||||
u16 d = ((u16*)gfx->framebuffer)[dest_idx];
|
||||
u16 m = (u16)(-(s != 0));
|
||||
((u16*)gfx->framebuffer)[dest_idx] = (s & m) | (d & ~m);
|
||||
u16* fb16 = (u16*)gfx->framebuffer;
|
||||
fb16[dest_idx] = pxl8_blend_hicolor(((const u16*)atlas_pixels)[src_idx], fb16[dest_idx]);
|
||||
} else {
|
||||
u8 s = atlas_pixels[src_idx];
|
||||
u8 d = gfx->framebuffer[dest_idx];
|
||||
u8 m = (u8)(-(s != 0));
|
||||
gfx->framebuffer[dest_idx] = (s & m) | (d & ~m);
|
||||
gfx->framebuffer[dest_idx] = pxl8_blend_indexed(atlas_pixels[src_idx], gfx->framebuffer[dest_idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,11 @@ typedef struct pxl8_hal {
|
|||
void (*set_relative_mouse_mode)(void* platform_data, bool enabled);
|
||||
void (*upload_texture)(void* platform_data, const u8* pixels, u32 w, u32 h,
|
||||
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;
|
||||
|
|
|
|||
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 { int x, y, w, h; } pxl8_bounds;\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"
|
||||
"f32 pxl8_get_fps(const pxl8* sys);\n"
|
||||
"\n"
|
||||
|
|
@ -266,7 +272,7 @@ static const char* pxl8_ffi_cdefs =
|
|||
"} pxl8_raster_bar;\n"
|
||||
"\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_clear(pxl8_particles* particles);\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_node_destroy(pxl8_sfx_node* node);\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"
|
||||
"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"
|
||||
|
|
@ -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) {
|
||||
if (!script) return;
|
||||
script->mixer = mixer;
|
||||
|
|
@ -747,6 +761,53 @@ pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) {
|
|||
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) {
|
||||
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);
|
||||
|
||||
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) {
|
||||
|
|
@ -985,7 +1049,7 @@ static bool pxl8_script_is_builtin_global(const char* name) {
|
|||
"print", "pxl8", "rawequal", "rawget", "rawset", "require",
|
||||
"select", "setfenv", "setmetatable", "string", "table",
|
||||
"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
|
||||
};
|
||||
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) {
|
||||
lua_State* L = script->L;
|
||||
|
||||
|
|
@ -1032,6 +1147,9 @@ static void pxl8_script_save_globals(pxl8_script* script) {
|
|||
}
|
||||
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");
|
||||
pxl8_debug("Hot reload state saved");
|
||||
}
|
||||
|
|
@ -1045,13 +1163,19 @@ static void pxl8_script_restore_globals(pxl8_script* script) {
|
|||
return;
|
||||
}
|
||||
|
||||
int saved_table = lua_gettop(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_settable(L, LUA_GLOBALSINDEX);
|
||||
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_pushnil(L);
|
||||
|
|
@ -1059,51 +1183,224 @@ static void pxl8_script_restore_globals(pxl8_script* script) {
|
|||
pxl8_debug("Hot reload state restored");
|
||||
}
|
||||
|
||||
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;
|
||||
#define SER_NIL 0
|
||||
#define SER_BOOL 1
|
||||
#define SER_NUMBER 2
|
||||
#define SER_STRING 3
|
||||
#define SER_TABLE 4
|
||||
|
||||
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) {
|
||||
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;
|
||||
static void ser_buffer_grow(ser_buffer* buf, u32 needed) {
|
||||
if (buf->size + needed > buf->capacity) {
|
||||
while (buf->size + needed > buf->capacity) {
|
||||
buf->capacity *= 2;
|
||||
}
|
||||
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 {
|
||||
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;
|
||||
lua_pushnil(L);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return latest;
|
||||
void pxl8_script_deserialize_globals(pxl8_script* script, const u8* data, u32 size) {
|
||||
if (!script || !data || size == 0) return;
|
||||
lua_State* L = script->L;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
pxl8_debug("Deserialized globals from %u bytes", size);
|
||||
}
|
||||
|
||||
void pxl8_script_free_serialized(u8* data) {
|
||||
free(data);
|
||||
}
|
||||
|
||||
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 (pxl8_script_run_fennel_file(script, script->main_path) == PXL8_OK) {
|
||||
pxl8_script_call_function(script, "init");
|
||||
reloaded = true;
|
||||
}
|
||||
} else if (ext && strcmp(ext, ".lua") == 0) {
|
||||
if (pxl8_script_run_file(script, script->main_path) == PXL8_OK) {
|
||||
pxl8_script_call_function(script, "init");
|
||||
reloaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (reloaded) {
|
||||
pxl8_script_restore_globals(script);
|
||||
pxl8_script_call_function(script, "init");
|
||||
}
|
||||
|
||||
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_gfx(pxl8_script* script, pxl8_gfx* gfx);
|
||||
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_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_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
|
||||
}
|
||||
#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 = {
|
||||
.create = sdl3_create,
|
||||
.destroy = sdl3_destroy,
|
||||
|
|
@ -325,6 +394,12 @@ const pxl8_hal pxl8_hal_sdl3 = {
|
|||
.center_cursor = sdl3_center_cursor,
|
||||
.present = sdl3_present,
|
||||
.set_cursor = sdl3_set_cursor,
|
||||
.upload_texture = sdl3_upload_texture,
|
||||
.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 <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "pxl8_hal.h"
|
||||
#include "pxl8_log.h"
|
||||
#include "pxl8_math.h"
|
||||
|
||||
|
|
@ -62,6 +61,8 @@ typedef struct voice {
|
|||
f32 filter_env_depth;
|
||||
f32 fx_send;
|
||||
f32 start_time;
|
||||
f32 duration;
|
||||
bool released;
|
||||
oscillator osc;
|
||||
envelope amp_env;
|
||||
envelope filter_env;
|
||||
|
|
@ -125,16 +126,21 @@ struct pxl8_sfx_node {
|
|||
struct pxl8_sfx_context {
|
||||
voice voices[PXL8_SFX_MAX_VOICES];
|
||||
pxl8_sfx_node* fx_head;
|
||||
pxl8_sfx_mixer* mixer;
|
||||
f32 volume;
|
||||
f32 current_time;
|
||||
u16 next_voice_id;
|
||||
u8 context_id;
|
||||
};
|
||||
|
||||
struct pxl8_sfx_mixer {
|
||||
SDL_AudioStream* stream;
|
||||
const pxl8_hal* hal;
|
||||
void* audio_handle;
|
||||
pxl8_sfx_context* contexts[PXL8_SFX_MAX_CONTEXTS];
|
||||
f32 master_volume;
|
||||
f32* output_buffer;
|
||||
pxl8_sfx_event_callback event_callback;
|
||||
void* event_userdata;
|
||||
};
|
||||
|
||||
static void envelope_trigger(envelope* env) {
|
||||
|
|
@ -334,7 +340,7 @@ static f32 lfo_process(lfo* l) {
|
|||
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->id = id;
|
||||
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->fx_send = params->fx_send;
|
||||
v->start_time = time;
|
||||
v->duration = duration;
|
||||
v->released = false;
|
||||
|
||||
oscillator_init(&v->osc, params->waveform, v->base_frequency);
|
||||
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;
|
||||
|
||||
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;
|
||||
voice_process(&ctx->voices[v], &dl, &dr, &wl, &wr);
|
||||
voice_process(vp, &dl, &dr, &wl, &wr);
|
||||
|
||||
dry_left += dl;
|
||||
dry_right += dr;
|
||||
|
|
@ -631,16 +646,58 @@ static void context_process_sample(pxl8_sfx_context* ctx, f32* out_left, f32* ou
|
|||
*out_right = right;
|
||||
}
|
||||
|
||||
static void SDLCALL audio_callback(void* userdata, SDL_AudioStream* stream, int additional_amount, int total_amount) {
|
||||
(void)total_amount;
|
||||
pxl8_sfx_mixer* pxl8_sfx_mixer_create(const pxl8_hal* hal) {
|
||||
if (!hal || !hal->audio_create) return NULL;
|
||||
|
||||
pxl8_sfx_mixer* mixer = (pxl8_sfx_mixer*)userdata;
|
||||
if (!mixer || !mixer->output_buffer) return;
|
||||
pxl8_sfx_mixer* mixer = (pxl8_sfx_mixer*)calloc(1, sizeof(pxl8_sfx_mixer));
|
||||
if (!mixer) return NULL;
|
||||
|
||||
u32 samples_needed = additional_amount / (sizeof(f32) * 2);
|
||||
if (samples_needed > PXL8_SFX_BUFFER_SIZE) samples_needed = PXL8_SFX_BUFFER_SIZE;
|
||||
mixer->output_buffer = (f32*)calloc(PXL8_SFX_BUFFER_SIZE * 2, sizeof(f32));
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -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++) {
|
||||
if (!mixer->contexts[i]) {
|
||||
mixer->contexts[i] = ctx;
|
||||
ctx->mixer = mixer;
|
||||
ctx->context_id = (u8)i;
|
||||
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));
|
||||
}
|
||||
|
||||
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) {
|
||||
return mixer ? mixer->master_volume : 0.0f;
|
||||
}
|
||||
|
|
@ -1047,7 +1072,7 @@ static i32 find_free_voice(pxl8_sfx_context* ctx) {
|
|||
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;
|
||||
|
||||
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++;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "pxl8_hal.h"
|
||||
#include "pxl8_types.h"
|
||||
|
||||
#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_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_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_detach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx);
|
||||
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_node_destroy(pxl8_sfx_node* node);
|
||||
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);
|
||||
|
||||
pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg);
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define PXL8_RANDF() ((f32)rand() / (f32)RAND_MAX)
|
||||
#include "pxl8_math.h"
|
||||
|
||||
struct pxl8_particles {
|
||||
pxl8_particle* particles;
|
||||
pxl8_rng* rng;
|
||||
u32 alive_count;
|
||||
u32 count;
|
||||
u32 max_count;
|
||||
|
|
@ -22,7 +23,7 @@ struct pxl8_particles {
|
|||
f32 spawn_timer;
|
||||
|
||||
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* userdata;
|
||||
};
|
||||
|
|
@ -170,7 +171,7 @@ void pxl8_vfx_water_ripple(pxl8_gfx* gfx, f32* height_map, i32 drop_x, i32 drop_
|
|||
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));
|
||||
if (!particles) return NULL;
|
||||
|
||||
|
|
@ -180,6 +181,7 @@ pxl8_particles* pxl8_particles_create(u32 max_count) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
particles->rng = rng;
|
||||
particles->max_count = max_count;
|
||||
particles->drag = 0.98f;
|
||||
particles->gravity_y = 100.0f;
|
||||
|
|
@ -214,8 +216,8 @@ void pxl8_particles_emit(pxl8_particles* particles, u32 count) {
|
|||
pxl8_particle* p = &particles->particles[j];
|
||||
p->life = 1.0f;
|
||||
p->max_life = 1.0f;
|
||||
p->x = particles->x + ((PXL8_RANDF()) - 0.5f) * particles->spread_x;
|
||||
p->y = particles->y + ((PXL8_RANDF()) - 0.5f) * particles->spread_y;
|
||||
p->x = particles->x + (pxl8_rng_f32(particles->rng) - 0.5f) * particles->spread_x;
|
||||
p->y = particles->y + (pxl8_rng_f32(particles->rng) - 0.5f) * particles->spread_y;
|
||||
p->z = 0;
|
||||
p->vx = p->vy = p->vz = 0;
|
||||
p->ax = particles->gravity_x;
|
||||
|
|
@ -228,7 +230,7 @@ void pxl8_particles_emit(pxl8_particles* particles, u32 count) {
|
|||
p->flags = 1;
|
||||
|
||||
if (particles->spawn_fn) {
|
||||
particles->spawn_fn(p, particles->userdata);
|
||||
particles->spawn_fn(particles, p);
|
||||
}
|
||||
|
||||
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++) {
|
||||
pxl8_particle* p = &particles->particles[i];
|
||||
f32 angle = (PXL8_RANDF()) * 6.28f;
|
||||
f32 speed = force * (0.5f + (PXL8_RANDF()) * 0.5f);
|
||||
f32 angle = pxl8_rng_f32(particles->rng) * PXL8_TAU;
|
||||
f32 speed = force * (0.5f + pxl8_rng_f32(particles->rng) * 0.5f);
|
||||
|
||||
p->x = x;
|
||||
p->y = y;
|
||||
p->vx = cosf(angle) * speed;
|
||||
p->vy = sinf(angle) * speed;
|
||||
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->flags = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void fire_spawn(pxl8_particle* p, void* userdata) {
|
||||
uintptr_t palette_start = (uintptr_t)userdata;
|
||||
p->start_color = palette_start + 6 + (rand() % 3);
|
||||
static void fire_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||
uintptr_t palette_start = (uintptr_t)particles->userdata;
|
||||
p->start_color = palette_start + 6 + pxl8_rng_range(particles->rng, 0, 3);
|
||||
p->end_color = palette_start;
|
||||
p->color = p->start_color;
|
||||
p->max_life = 1.5f + (PXL8_RANDF()) * 1.5f;
|
||||
p->vy = -80.0f - (PXL8_RANDF()) * 120.0f;
|
||||
p->vx = ((PXL8_RANDF()) - 0.5f) * 40.0f;
|
||||
p->max_life = 1.5f + pxl8_rng_f32(particles->rng) * 1.5f;
|
||||
p->vy = -80.0f - pxl8_rng_f32(particles->rng) * 120.0f;
|
||||
p->vx = (pxl8_rng_f32(particles->rng) - 0.5f) * 40.0f;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void rain_spawn(pxl8_particle* p, void* userdata) {
|
||||
(void)userdata;
|
||||
p->start_color = 27 + (rand() % 3);
|
||||
static void rain_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||
p->start_color = 27 + pxl8_rng_range(particles->rng, 0, 3);
|
||||
p->end_color = 29;
|
||||
p->color = p->start_color;
|
||||
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) {
|
||||
|
|
@ -400,15 +401,15 @@ void pxl8_vfx_rain(pxl8_particles* particles, i32 width, f32 wind) {
|
|||
particles->update_fn = NULL;
|
||||
}
|
||||
|
||||
static void smoke_spawn(pxl8_particle* p, void* userdata) {
|
||||
uintptr_t base_color = (uintptr_t)userdata;
|
||||
static void smoke_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||
uintptr_t base_color = (uintptr_t)particles->userdata;
|
||||
p->start_color = base_color;
|
||||
p->end_color = base_color + 4;
|
||||
p->color = p->start_color;
|
||||
p->max_life = 3.0f + (PXL8_RANDF()) * 2.0f;
|
||||
p->vy = -20.0f - (PXL8_RANDF()) * 30.0f;
|
||||
p->vx = ((PXL8_RANDF()) - 0.5f) * 20.0f;
|
||||
p->size = 1.0f + (PXL8_RANDF()) * 2.0f;
|
||||
p->max_life = 3.0f + pxl8_rng_f32(particles->rng) * 2.0f;
|
||||
p->vy = -20.0f - pxl8_rng_f32(particles->rng) * 30.0f;
|
||||
p->vx = (pxl8_rng_f32(particles->rng) - 0.5f) * 20.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) {
|
||||
|
|
@ -427,14 +428,13 @@ void pxl8_vfx_smoke(pxl8_particles* particles, i32 x, i32 y, u8 color) {
|
|||
particles->userdata = (void*)(uintptr_t)color;
|
||||
}
|
||||
|
||||
static void snow_spawn(pxl8_particle* p, void* userdata) {
|
||||
(void)userdata;
|
||||
p->start_color = 8 + (rand() % 3);
|
||||
static void snow_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||
p->start_color = 8 + pxl8_rng_range(particles->rng, 0, 3);
|
||||
p->end_color = 10;
|
||||
p->color = p->start_color;
|
||||
p->max_life = 4.0f;
|
||||
p->vx = ((PXL8_RANDF()) - 0.5f) * 20.0f;
|
||||
p->vy = 30.0f + (PXL8_RANDF()) * 20.0f;
|
||||
p->vx = (pxl8_rng_f32(particles->rng) - 0.5f) * 20.0f;
|
||||
p->vy = 30.0f + pxl8_rng_f32(particles->rng) * 20.0f;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void sparks_spawn(pxl8_particle* p, void* userdata) {
|
||||
uintptr_t base_color = (uintptr_t)userdata;
|
||||
static void sparks_spawn(pxl8_particles* particles, pxl8_particle* p) {
|
||||
uintptr_t base_color = (uintptr_t)particles->userdata;
|
||||
p->start_color = base_color;
|
||||
p->end_color = base_color > 2 ? base_color - 2 : 0;
|
||||
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 speed = 100.0f + (PXL8_RANDF()) * 200.0f;
|
||||
f32 angle = pxl8_rng_f32(particles->rng) * PXL8_TAU;
|
||||
f32 speed = 100.0f + pxl8_rng_f32(particles->rng) * 200.0f;
|
||||
p->vx = cosf(angle) * speed;
|
||||
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++) {
|
||||
pxl8_particle* p = &particles->particles[i];
|
||||
p->x = (PXL8_RANDF()) * spread * 2.0f - spread;
|
||||
p->y = (PXL8_RANDF()) * spread * 2.0f - spread;
|
||||
p->z = (PXL8_RANDF()) * spread;
|
||||
p->x = pxl8_rng_f32(particles->rng) * spread * 2.0f - spread;
|
||||
p->y = pxl8_rng_f32(particles->rng) * spread * 2.0f - spread;
|
||||
p->z = pxl8_rng_f32(particles->rng) * spread;
|
||||
p->vz = -speed;
|
||||
p->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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "pxl8_gfx.h"
|
||||
#include "pxl8_rng.h"
|
||||
#include "pxl8_types.h"
|
||||
|
||||
typedef struct pxl8_particles pxl8_particles;
|
||||
|
|
@ -34,7 +35,7 @@ typedef struct pxl8_raster_bar {
|
|||
extern "C" {
|
||||
#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_clear(pxl8_particles* particles);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue