refactor: reorganize pxl8 into client/src/ module structure

- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
This commit is contained in:
asrael 2026-01-12 21:46:31 -06:00
parent 272e0bc615
commit 39b604b333
106 changed files with 6078 additions and 3715 deletions

View file

@ -1,248 +0,0 @@
local core = require("pxl8.core")
local gfx2d = require("pxl8.gfx2d")
local input = require("pxl8.input")
local vfx = require("pxl8.vfx")
local particles = require("pxl8.particles")
local tilemap = require("pxl8.tilemap")
local gfx3d = require("pxl8.gfx3d")
local math3d = require("pxl8.math")
local gui = require("pxl8.gui")
local world = require("pxl8.world")
local transition = require("pxl8.transition")
local anim = require("pxl8.anim")
local sfx = require("pxl8.sfx")
local pxl8 = {}
core.init(pxl8_gfx, pxl8_input, pxl8_rng, pxl8_sfx, pxl8_sys)
pxl8.get_fps = core.get_fps
pxl8.get_height = core.get_height
pxl8.get_title = core.get_title
pxl8.get_width = core.get_width
pxl8.info = core.info
pxl8.warn = core.warn
pxl8.error = core.error
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
pxl8.rect = gfx2d.rect
pxl8.rect_fill = gfx2d.rect_fill
pxl8.circle = gfx2d.circle
pxl8.circle_fill = gfx2d.circle_fill
pxl8.text = gfx2d.text
pxl8.sprite = gfx2d.sprite
pxl8.load_palette = gfx2d.load_palette
pxl8.load_sprite = gfx2d.load_sprite
pxl8.create_texture = gfx2d.create_texture
pxl8.gfx_color_ramp = gfx2d.color_ramp
pxl8.gfx_fade_palette = gfx2d.fade_palette
pxl8.gfx_cycle_palette = gfx2d.cycle_palette
pxl8.add_palette_cycle = gfx2d.add_palette_cycle
pxl8.remove_palette_cycle = gfx2d.remove_palette_cycle
pxl8.set_palette_cycle_speed = gfx2d.set_palette_cycle_speed
pxl8.clear_palette_cycles = gfx2d.clear_palette_cycles
pxl8.key_down = input.key_down
pxl8.key_pressed = input.key_pressed
pxl8.key_released = input.key_released
pxl8.mouse_dx = input.mouse_dx
pxl8.mouse_dy = input.mouse_dy
pxl8.mouse_wheel_x = input.mouse_wheel_x
pxl8.mouse_wheel_y = input.mouse_wheel_y
pxl8.mouse_x = input.mouse_x
pxl8.mouse_y = input.mouse_y
pxl8.get_mouse_pos = input.get_mouse_pos
pxl8.mouse_pressed = input.mouse_pressed
pxl8.mouse_released = input.mouse_released
pxl8.center_cursor = input.center_cursor
pxl8.set_cursor = input.set_cursor
pxl8.set_relative_mouse_mode = input.set_relative_mouse_mode
pxl8.vfx_raster_bars = vfx.raster_bars
pxl8.vfx_plasma = vfx.plasma
pxl8.vfx_rotozoom = vfx.rotozoom
pxl8.vfx_tunnel = vfx.tunnel
pxl8.vfx_explosion = vfx.explosion
pxl8.vfx_fire = vfx.fire
pxl8.vfx_rain = vfx.rain
pxl8.vfx_smoke = vfx.smoke
pxl8.vfx_snow = vfx.snow
pxl8.vfx_sparks = vfx.sparks
pxl8.vfx_starfield = vfx.starfield
pxl8.particles_new = particles.new
pxl8.particles_destroy = particles.destroy
pxl8.particles_clear = particles.clear
pxl8.particles_emit = particles.emit
pxl8.particles_update = particles.update
pxl8.particles_render = particles.render
pxl8.tilesheet_new = tilemap.tilesheet_new
pxl8.tilesheet_destroy = tilemap.tilesheet_destroy
pxl8.tilesheet_load = tilemap.tilesheet_load
pxl8.tilemap_new = tilemap.new
pxl8.tilemap_destroy = tilemap.destroy
pxl8.tilemap_set_tilesheet = tilemap.set_tilesheet
pxl8.tilemap_set_tile = tilemap.set_tile
pxl8.tilemap_get_tile_id = tilemap.get_tile_id
pxl8.tilemap_set_camera = tilemap.set_camera
pxl8.tilemap_render = tilemap.render
pxl8.tilemap_render_layer = tilemap.render_layer
pxl8.tilemap_is_solid = tilemap.is_solid
pxl8.tilemap_check_collision = tilemap.check_collision
pxl8.tilemap_get_tile_data = tilemap.get_tile_data
pxl8.tilemap_load_ase = tilemap.load_ase
pxl8.tilemap_set_tile_data = tilemap.set_tile_data
pxl8.TILE_FLIP_X = tilemap.TILE_FLIP_X
pxl8.TILE_FLIP_Y = tilemap.TILE_FLIP_Y
pxl8.TILE_SOLID = tilemap.TILE_SOLID
pxl8.TILE_TRIGGER = tilemap.TILE_TRIGGER
pxl8.clear_zbuffer = gfx3d.clear_zbuffer
pxl8.set_model = gfx3d.set_model
pxl8.set_view = gfx3d.set_view
pxl8.set_projection = gfx3d.set_projection
pxl8.set_wireframe = gfx3d.set_wireframe
pxl8.set_affine_textures = gfx3d.set_affine_textures
pxl8.set_backface_culling = gfx3d.set_backface_culling
pxl8.draw_triangle_3d = gfx3d.draw_triangle
pxl8.draw_triangle_3d_textured = gfx3d.draw_triangle_textured
pxl8.draw_line_3d = gfx3d.draw_line
pxl8.mat4_identity = math3d.mat4_identity
pxl8.mat4_multiply = math3d.mat4_multiply
pxl8.mat4_translate = math3d.mat4_translate
pxl8.mat4_rotate_x = math3d.mat4_rotate_x
pxl8.mat4_rotate_y = math3d.mat4_rotate_y
pxl8.mat4_rotate_z = math3d.mat4_rotate_z
pxl8.mat4_scale = math3d.mat4_scale
pxl8.mat4_ortho = math3d.mat4_ortho
pxl8.mat4_perspective = math3d.mat4_perspective
pxl8.mat4_lookat = math3d.mat4_lookat
pxl8.bounds = math3d.bounds
pxl8.gui_state_create = gui.state_create
pxl8.gui_state_destroy = gui.state_destroy
pxl8.gui_begin_frame = gui.begin_frame
pxl8.gui_end_frame = gui.end_frame
pxl8.gui_cursor_move = gui.cursor_move
pxl8.gui_cursor_down = gui.cursor_down
pxl8.gui_cursor_up = gui.cursor_up
pxl8.gui_button = gui.button
pxl8.gui_window = gui.window
pxl8.gui_label = gui.label
pxl8.gui_is_hovering = gui.is_hovering
pxl8.gui_get_cursor_pos = gui.get_cursor_pos
pxl8.world_new = world.new
pxl8.world_destroy = world.destroy
pxl8.world_load = world.load
pxl8.world_render = world.render
pxl8.world_unload = world.unload
pxl8.world_is_loaded = world.is_loaded
pxl8.world_generate = world.generate
pxl8.world_apply_textures = world.apply_textures
pxl8.world_check_collision = world.check_collision
pxl8.world_resolve_collision = world.resolve_collision
pxl8.procgen_tex = world.procgen_tex
pxl8.PROCGEN_ROOMS = world.PROCGEN_ROOMS
pxl8.PROCGEN_TERRAIN = world.PROCGEN_TERRAIN
pxl8.transition_create = transition.create
pxl8.transition_destroy = transition.destroy
pxl8.transition_get_progress = transition.get_progress
pxl8.transition_is_active = transition.is_active
pxl8.transition_is_complete = transition.is_complete
pxl8.transition_render = transition.render
pxl8.transition_reset = transition.reset
pxl8.transition_set_color = transition.set_color
pxl8.transition_set_reverse = transition.set_reverse
pxl8.transition_start = transition.start
pxl8.transition_stop = transition.stop
pxl8.transition_update = transition.update
pxl8.anim_create = anim.create
pxl8.anim_create_from_ase = anim.create_from_ase
pxl8.anim_destroy = anim.destroy
pxl8.anim_add_state = anim.add_state
pxl8.anim_get_current_frame = anim.get_current_frame
pxl8.anim_get_current_frame_id = anim.get_current_frame_id
pxl8.anim_get_state = anim.get_state
pxl8.anim_has_state_machine = anim.has_state_machine
pxl8.anim_is_complete = anim.is_complete
pxl8.anim_is_playing = anim.is_playing
pxl8.anim_pause = anim.pause
pxl8.anim_play = anim.play
pxl8.anim_render_sprite = anim.render_sprite
pxl8.anim_reset = anim.reset
pxl8.anim_set_frame = anim.set_frame
pxl8.anim_set_loop = anim.set_loop
pxl8.anim_set_reverse = anim.set_reverse
pxl8.anim_set_speed = anim.set_speed
pxl8.anim_set_state = anim.set_state
pxl8.anim_stop = anim.stop
pxl8.anim_update = anim.update
pxl8.sfx_compressor_create = sfx.compressor_create
pxl8.sfx_compressor_set_attack = sfx.compressor_set_attack
pxl8.sfx_compressor_set_ratio = sfx.compressor_set_ratio
pxl8.sfx_compressor_set_release = sfx.compressor_set_release
pxl8.sfx_compressor_set_threshold = sfx.compressor_set_threshold
pxl8.sfx_context_append_node = sfx.context_append_node
pxl8.sfx_context_create = sfx.context_create
pxl8.sfx_context_destroy = sfx.context_destroy
pxl8.sfx_context_get_head = sfx.context_get_head
pxl8.sfx_context_get_volume = sfx.context_get_volume
pxl8.sfx_context_insert_node = sfx.context_insert_node
pxl8.sfx_context_remove_node = sfx.context_remove_node
pxl8.sfx_context_set_volume = sfx.context_set_volume
pxl8.sfx_delay_create = sfx.delay_create
pxl8.sfx_delay_set_feedback = sfx.delay_set_feedback
pxl8.sfx_delay_set_mix = sfx.delay_set_mix
pxl8.sfx_delay_set_time = sfx.delay_set_time
pxl8.sfx_get_master_volume = sfx.get_master_volume
pxl8.sfx_mixer_attach = sfx.mixer_attach
pxl8.sfx_mixer_detach = sfx.mixer_detach
pxl8.sfx_node_destroy = sfx.node_destroy
pxl8.sfx_note_to_freq = sfx.note_to_freq
pxl8.sfx_play_note = sfx.play_note
pxl8.sfx_release_voice = sfx.release_voice
pxl8.sfx_reverb_create = sfx.reverb_create
pxl8.sfx_reverb_set_damping = sfx.reverb_set_damping
pxl8.sfx_reverb_set_mix = sfx.reverb_set_mix
pxl8.sfx_reverb_set_room = sfx.reverb_set_room
pxl8.sfx_set_master_volume = sfx.set_master_volume
pxl8.sfx_stop_all = sfx.stop_all
pxl8.sfx_stop_voice = sfx.stop_voice
pxl8.sfx_voice_params = sfx.voice_params
pxl8.SFX_FILTER_BANDPASS = sfx.FILTER_BANDPASS
pxl8.SFX_FILTER_HIGHPASS = sfx.FILTER_HIGHPASS
pxl8.SFX_FILTER_LOWPASS = sfx.FILTER_LOWPASS
pxl8.SFX_FILTER_NONE = sfx.FILTER_NONE
pxl8.SFX_LFO_AMPLITUDE = sfx.LFO_AMPLITUDE
pxl8.SFX_LFO_FILTER = sfx.LFO_FILTER
pxl8.SFX_LFO_PITCH = sfx.LFO_PITCH
pxl8.SFX_NODE_COMPRESSOR = sfx.NODE_COMPRESSOR
pxl8.SFX_NODE_DELAY = sfx.NODE_DELAY
pxl8.SFX_NODE_REVERB = sfx.NODE_REVERB
pxl8.SFX_WAVE_NOISE = sfx.WAVE_NOISE
pxl8.SFX_WAVE_PULSE = sfx.WAVE_PULSE
pxl8.SFX_WAVE_SAW = sfx.WAVE_SAW
pxl8.SFX_WAVE_SINE = sfx.WAVE_SINE
pxl8.SFX_WAVE_SQUARE = sfx.WAVE_SQUARE
pxl8.SFX_WAVE_TRIANGLE = sfx.WAVE_TRIANGLE
return pxl8

View file

@ -1,110 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local anim = {}
function anim.create(frame_ids, frame_durations)
local frame_count = #frame_ids
local c_frame_ids = ffi.new("u32[?]", frame_count)
local c_frame_durations = nil
for i = 1, frame_count do
c_frame_ids[i-1] = frame_ids[i]
end
if frame_durations then
c_frame_durations = ffi.new("u16[?]", frame_count)
for i = 1, frame_count do
c_frame_durations[i-1] = frame_durations[i]
end
end
return C.pxl8_anim_create(c_frame_ids, c_frame_durations, frame_count)
end
function anim.create_from_ase(filepath)
return C.pxl8_anim_create_from_ase(core.gfx, filepath)
end
function anim.destroy(a)
C.pxl8_anim_destroy(a)
end
function anim.add_state(a, name, state_anim)
return C.pxl8_anim_add_state(a, name, state_anim)
end
function anim.get_current_frame(a)
return C.pxl8_anim_get_current_frame(a)
end
function anim.get_current_frame_id(a)
return C.pxl8_anim_get_current_frame_id(a)
end
function anim.get_state(a)
local state_name = C.pxl8_anim_get_state(a)
if state_name ~= nil then
return ffi.string(state_name)
end
return nil
end
function anim.has_state_machine(a)
return C.pxl8_anim_has_state_machine(a)
end
function anim.is_complete(a)
return C.pxl8_anim_is_complete(a)
end
function anim.is_playing(a)
return C.pxl8_anim_is_playing(a)
end
function anim.pause(a)
C.pxl8_anim_pause(a)
end
function anim.play(a)
C.pxl8_anim_play(a)
end
function anim.render_sprite(a, x, y, w, h, flip_x, flip_y)
C.pxl8_anim_render_sprite(a, core.gfx, x, y, w, h, flip_x or false, flip_y or false)
end
function anim.reset(a)
C.pxl8_anim_reset(a)
end
function anim.set_frame(a, frame)
C.pxl8_anim_set_frame(a, frame)
end
function anim.set_loop(a, loop)
C.pxl8_anim_set_loop(a, loop)
end
function anim.set_reverse(a, reverse)
C.pxl8_anim_set_reverse(a, reverse)
end
function anim.set_speed(a, speed)
C.pxl8_anim_set_speed(a, speed)
end
function anim.set_state(a, name)
return C.pxl8_anim_set_state(a, name)
end
function anim.stop(a)
C.pxl8_anim_stop(a)
end
function anim.update(a, dt)
C.pxl8_anim_update(a, dt)
end
return anim

View file

@ -1,92 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = {}
function core.init(gfx, input, rng, sfx, sys)
core.gfx = gfx
core.input = input
core.rng = rng
core.sfx = sfx
core.sys = sys
end
function core.get_fps()
return C.pxl8_get_fps(core.sys)
end
function core.get_width()
return C.pxl8_gfx_get_width(core.gfx)
end
function core.get_height()
return C.pxl8_gfx_get_height(core.gfx)
end
function core.get_title()
local cart = C.pxl8_get_cart()
if cart ~= nil then
local title = C.pxl8_cart_get_title(cart)
if title ~= nil then
return ffi.string(title)
end
end
return "pxl8"
end
local function is_user_script(info)
local src = info and info.short_src
return src and (src:match("%.fnl$") or src:match("%.lua$"))
end
local function get_caller_info()
for level = 2, 10 do
local info = debug.getinfo(level, "Sl")
if not info then break end
if is_user_script(info) then
return info.short_src, info.currentline or 0
end
end
return "?", 0
end
core.LOG_TRACE = 0
core.LOG_DEBUG = 1
core.LOG_INFO = 2
core.LOG_WARN = 3
core.LOG_ERROR = 4
local function make_logger(level)
return function(msg)
local src, line = get_caller_info()
C.pxl8_lua_log(level, src, line, msg)
end
end
core.trace = make_logger(core.LOG_TRACE)
core.debug = make_logger(core.LOG_DEBUG)
core.info = make_logger(core.LOG_INFO)
core.warn = make_logger(core.LOG_WARN)
core.error = make_logger(core.LOG_ERROR)
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

View file

@ -1,101 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local graphics = {}
function graphics.clear(color)
C.pxl8_clear(core.gfx, color or 0)
end
function graphics.pixel(x, y, color)
if color then
C.pxl8_pixel(core.gfx, x, y, color)
else
return C.pxl8_get_pixel(core.gfx, x, y)
end
end
function graphics.line(x0, y0, x1, y1, color)
C.pxl8_line(core.gfx, x0, y0, x1, y1, color)
end
function graphics.rect(x, y, w, h, color)
C.pxl8_rect(core.gfx, x, y, w, h, color)
end
function graphics.rect_fill(x, y, w, h, color)
C.pxl8_rect_fill(core.gfx, x, y, w, h, color)
end
function graphics.circle(x, y, r, color)
C.pxl8_circle(core.gfx, x, y, r, color)
end
function graphics.circle_fill(x, y, r, color)
C.pxl8_circle_fill(core.gfx, x, y, r, color)
end
function graphics.text(str, x, y, color)
C.pxl8_text(core.gfx, str, x or 0, y or 0, color or 15)
end
function graphics.sprite(id, x, y, w, h, flip_x, flip_y)
C.pxl8_sprite(core.gfx, id or 0, x or 0, y or 0, w or 16, h or 16, flip_x or false, flip_y or false)
end
function graphics.load_palette(filepath)
return C.pxl8_gfx_load_palette(core.gfx, filepath)
end
function graphics.load_sprite(filepath)
local sprite_id = ffi.new("unsigned int[1]")
local result = C.pxl8_gfx_load_sprite(core.gfx, filepath, sprite_id)
if result == 0 then
return sprite_id[0]
else
return nil, result
end
end
function graphics.create_texture(pixels, width, height)
local pixel_data = ffi.new("u8[?]", width * height)
for i = 0, width * height - 1 do
pixel_data[i] = pixels[i + 1] or 0
end
local result = C.pxl8_gfx_create_texture(core.gfx, pixel_data, width, height)
if result < 0 then
return nil
end
return result
end
function graphics.color_ramp(start, count, from_color, to_color)
C.pxl8_gfx_color_ramp(core.gfx, start, count, from_color, to_color)
end
function graphics.fade_palette(start, count, amount, target_color)
C.pxl8_gfx_fade_palette(core.gfx, start, count, amount, target_color)
end
function graphics.cycle_palette(start, count, step)
C.pxl8_gfx_cycle_palette(core.gfx, start, count, step or 1)
end
function graphics.add_palette_cycle(start_index, end_index, speed)
return C.pxl8_gfx_add_palette_cycle(core.gfx, start_index, end_index, speed or 1.0)
end
function graphics.remove_palette_cycle(cycle_id)
C.pxl8_gfx_remove_palette_cycle(core.gfx, cycle_id)
end
function graphics.set_palette_cycle_speed(cycle_id, speed)
C.pxl8_gfx_set_palette_cycle_speed(core.gfx, cycle_id, speed)
end
function graphics.clear_palette_cycles()
C.pxl8_gfx_clear_palette_cycles(core.gfx)
end
return graphics

View file

@ -1,56 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local gfx3d = {}
function gfx3d.clear_zbuffer()
C.pxl8_3d_clear_zbuffer(core.gfx)
end
function gfx3d.set_model(mat)
C.pxl8_3d_set_model(core.gfx, mat)
end
function gfx3d.set_view(mat)
C.pxl8_3d_set_view(core.gfx, mat)
end
function gfx3d.set_projection(mat)
C.pxl8_3d_set_projection(core.gfx, mat)
end
function gfx3d.set_wireframe(wireframe)
C.pxl8_3d_set_wireframe(core.gfx, wireframe)
end
function gfx3d.set_affine_textures(affine)
C.pxl8_3d_set_affine_textures(core.gfx, affine)
end
function gfx3d.set_backface_culling(culling)
C.pxl8_3d_set_backface_culling(core.gfx, culling)
end
function gfx3d.draw_triangle(v0, v1, v2, color)
local vec0 = ffi.new("pxl8_vec3", {x = v0[1], y = v0[2], z = v0[3]})
local vec1 = ffi.new("pxl8_vec3", {x = v1[1], y = v1[2], z = v1[3]})
local vec2 = ffi.new("pxl8_vec3", {x = v2[1], y = v2[2], z = v2[3]})
C.pxl8_3d_draw_triangle_raw(core.gfx, vec0, vec1, vec2, color)
end
function gfx3d.draw_triangle_textured(v0, v1, v2, uv0, uv1, uv2, texture_id)
local vec0 = ffi.new("pxl8_vec3", {x = v0[1], y = v0[2], z = v0[3]})
local vec1 = ffi.new("pxl8_vec3", {x = v1[1], y = v1[2], z = v1[3]})
local vec2 = ffi.new("pxl8_vec3", {x = v2[1], y = v2[2], z = v2[3]})
C.pxl8_3d_draw_triangle_textured(core.gfx, vec0, vec1, vec2,
uv0[1], uv0[2], uv1[1], uv1[2], uv2[1], uv2[2], texture_id)
end
function gfx3d.draw_line(p0, p1, color)
local vec0 = ffi.new("pxl8_vec3", {x = p0[1], y = p0[2], z = p0[3]})
local vec1 = ffi.new("pxl8_vec3", {x = p1[1], y = p1[2], z = p1[3]})
C.pxl8_3d_draw_line_3d(core.gfx, vec0, vec1, color)
end
return gfx3d

View file

@ -1,59 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local gui = {}
local state = nil
local function gui_state()
if not state then
state = ffi.gc(C.pxl8_gui_state_create(), C.pxl8_gui_state_destroy)
end
return state
end
function gui.begin_frame()
C.pxl8_gui_begin_frame(gui_state())
end
function gui.end_frame()
C.pxl8_gui_end_frame(gui_state())
end
function gui.cursor_move(x, y)
C.pxl8_gui_cursor_move(gui_state(), x, y)
end
function gui.cursor_down()
C.pxl8_gui_cursor_down(gui_state())
end
function gui.cursor_up()
C.pxl8_gui_cursor_up(gui_state())
end
function gui.button(id, x, y, w, h, label)
return C.pxl8_gui_button(gui_state(), core.gfx, id, x, y, w, h, label)
end
function gui.window(x, y, w, h, title)
C.pxl8_gui_window(core.gfx, x, y, w, h, title)
end
function gui.label(x, y, text, color)
C.pxl8_gui_label(core.gfx, x, y, text, color)
end
function gui.is_hovering()
return C.pxl8_gui_is_hovering(gui_state())
end
function gui.get_cursor_pos()
local x = ffi.new("i32[1]")
local y = ffi.new("i32[1]")
C.pxl8_gui_get_cursor_pos(gui_state(), x, y)
return x[0], y[0]
end
return gui

View file

@ -1,75 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local input = {}
function input.key_down(key)
return C.pxl8_key_down(core.input, key)
end
function input.key_pressed(key)
return C.pxl8_key_pressed(core.input, key)
end
function input.key_released(key)
return C.pxl8_key_released(core.input, key)
end
function input.mouse_wheel_x()
return C.pxl8_mouse_wheel_x(core.input)
end
function input.mouse_wheel_y()
return C.pxl8_mouse_wheel_y(core.input)
end
function input.mouse_x()
return C.pxl8_mouse_x(core.input)
end
function input.mouse_y()
return C.pxl8_mouse_y(core.input)
end
function input.mouse_dx()
return C.pxl8_mouse_dx(core.input)
end
function input.mouse_dy()
return C.pxl8_mouse_dy(core.input)
end
function input.get_mouse_pos()
return C.pxl8_mouse_x(core.input), C.pxl8_mouse_y(core.input)
end
function input.mouse_pressed(button)
return C.pxl8_mouse_pressed(core.input, button)
end
function input.mouse_released(button)
return C.pxl8_mouse_released(core.input, button)
end
function input.set_relative_mouse_mode(enabled)
C.pxl8_set_relative_mouse_mode(core.sys, enabled)
end
function input.center_cursor()
C.pxl8_center_cursor(core.sys)
end
function input.set_cursor(cursor_type)
local cursor_enum
if cursor_type == "arrow" then
cursor_enum = C.PXL8_CURSOR_ARROW
elseif cursor_type == "hand" then
cursor_enum = C.PXL8_CURSOR_HAND
else
cursor_enum = C.PXL8_CURSOR_ARROW
end
C.pxl8_set_cursor(core.sys, cursor_enum)
end
return input

View file

@ -1,53 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local math3d = {}
function math3d.mat4_identity()
return C.pxl8_mat4_identity()
end
function math3d.mat4_multiply(a, b)
return C.pxl8_mat4_multiply(a, b)
end
function math3d.mat4_translate(x, y, z)
return C.pxl8_mat4_translate(x, y, z)
end
function math3d.mat4_rotate_x(angle)
return C.pxl8_mat4_rotate_x(angle)
end
function math3d.mat4_rotate_y(angle)
return C.pxl8_mat4_rotate_y(angle)
end
function math3d.mat4_rotate_z(angle)
return C.pxl8_mat4_rotate_z(angle)
end
function math3d.mat4_scale(x, y, z)
return C.pxl8_mat4_scale(x, y, z)
end
function math3d.mat4_ortho(left, right, bottom, top, near, far)
return C.pxl8_mat4_ortho(left, right, bottom, top, near, far)
end
function math3d.mat4_perspective(fov, aspect, near, far)
return C.pxl8_mat4_perspective(fov, aspect, near, far)
end
function math3d.mat4_lookat(eye, center, up)
local eye_vec = ffi.new("pxl8_vec3", {x = eye[1], y = eye[2], z = eye[3]})
local center_vec = ffi.new("pxl8_vec3", {x = center[1], y = center[2], z = center[3]})
local up_vec = ffi.new("pxl8_vec3", {x = up[1], y = up[2], z = up[3]})
return C.pxl8_mat4_lookat(eye_vec, center_vec, up_vec)
end
function math3d.bounds(x, y, w, h)
return ffi.new("pxl8_bounds", {x = x, y = y, w = w, h = h})
end
return math3d

View file

@ -1,31 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local particles = {}
function particles.new(max_count)
return C.pxl8_particles_create(max_count or 1000, core.rng)
end
function particles.destroy(ps)
C.pxl8_particles_destroy(ps)
end
function particles.clear(ps)
C.pxl8_particles_clear(ps)
end
function particles.emit(ps, count)
C.pxl8_particles_emit(ps, count or 1)
end
function particles.update(ps, dt)
C.pxl8_particles_update(ps, dt)
end
function particles.render(ps)
C.pxl8_particles_render(ps, core.gfx)
end
return particles

View file

@ -1,198 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local sfx = {}
sfx.FILTER_BANDPASS = C.PXL8_SFX_FILTER_BANDPASS
sfx.FILTER_HIGHPASS = C.PXL8_SFX_FILTER_HIGHPASS
sfx.FILTER_LOWPASS = C.PXL8_SFX_FILTER_LOWPASS
sfx.FILTER_NONE = C.PXL8_SFX_FILTER_NONE
sfx.LFO_AMPLITUDE = C.PXL8_SFX_LFO_AMPLITUDE
sfx.LFO_FILTER = C.PXL8_SFX_LFO_FILTER
sfx.LFO_PITCH = C.PXL8_SFX_LFO_PITCH
sfx.NODE_COMPRESSOR = C.PXL8_SFX_NODE_COMPRESSOR
sfx.NODE_DELAY = C.PXL8_SFX_NODE_DELAY
sfx.NODE_REVERB = C.PXL8_SFX_NODE_REVERB
sfx.WAVE_NOISE = C.PXL8_SFX_WAVE_NOISE
sfx.WAVE_PULSE = C.PXL8_SFX_WAVE_PULSE
sfx.WAVE_SAW = C.PXL8_SFX_WAVE_SAW
sfx.WAVE_SINE = C.PXL8_SFX_WAVE_SINE
sfx.WAVE_SQUARE = C.PXL8_SFX_WAVE_SQUARE
sfx.WAVE_TRIANGLE = C.PXL8_SFX_WAVE_TRIANGLE
function sfx.compressor_create(opts)
opts = opts or {}
local cfg = ffi.new("pxl8_sfx_compressor_config")
cfg.attack = opts.attack or 10
cfg.ratio = opts.ratio or 4
cfg.release = opts.release or 100
cfg.threshold = opts.threshold or -12
return C.pxl8_sfx_compressor_create(cfg)
end
function sfx.compressor_set_attack(node, attack)
C.pxl8_sfx_compressor_set_attack(node, attack)
end
function sfx.compressor_set_ratio(node, ratio)
C.pxl8_sfx_compressor_set_ratio(node, ratio)
end
function sfx.compressor_set_release(node, release)
C.pxl8_sfx_compressor_set_release(node, release)
end
function sfx.compressor_set_threshold(node, threshold)
C.pxl8_sfx_compressor_set_threshold(node, threshold)
end
function sfx.context_append_node(ctx, node)
C.pxl8_sfx_context_append_node(ctx, node)
end
function sfx.context_create()
return C.pxl8_sfx_context_create()
end
function sfx.context_destroy(ctx)
C.pxl8_sfx_context_destroy(ctx)
end
function sfx.context_get_head(ctx)
return C.pxl8_sfx_context_get_head(ctx)
end
function sfx.context_get_volume(ctx)
return C.pxl8_sfx_context_get_volume(ctx)
end
function sfx.context_insert_node(ctx, after, node)
C.pxl8_sfx_context_insert_node(ctx, after, node)
end
function sfx.context_remove_node(ctx, node)
C.pxl8_sfx_context_remove_node(ctx, node)
end
function sfx.context_set_volume(ctx, volume)
C.pxl8_sfx_context_set_volume(ctx, volume)
end
function sfx.delay_create(opts)
opts = opts or {}
local cfg = ffi.new("pxl8_sfx_delay_config")
cfg.feedback = opts.feedback or 0.4
cfg.mix = opts.mix or 0.25
cfg.time_l = opts.time_l or 350
cfg.time_r = opts.time_r or 500
return C.pxl8_sfx_delay_create(cfg)
end
function sfx.delay_set_feedback(node, feedback)
C.pxl8_sfx_delay_set_feedback(node, feedback)
end
function sfx.delay_set_mix(node, mix)
C.pxl8_sfx_delay_set_mix(node, mix)
end
function sfx.delay_set_time(node, time_l, time_r)
C.pxl8_sfx_delay_set_time(node, time_l, time_r)
end
function sfx.get_master_volume()
return C.pxl8_sfx_mixer_get_master_volume(core.sfx)
end
function sfx.mixer_attach(ctx)
C.pxl8_sfx_mixer_attach(core.sfx, ctx)
end
function sfx.mixer_detach(ctx)
C.pxl8_sfx_mixer_detach(core.sfx, ctx)
end
function sfx.node_destroy(node)
C.pxl8_sfx_node_destroy(node)
end
function sfx.note_to_freq(note)
return C.pxl8_sfx_note_to_freq(note)
end
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)
C.pxl8_sfx_release_voice(ctx, voice_id)
end
function sfx.reverb_create(opts)
opts = opts or {}
local cfg = ffi.new("pxl8_sfx_reverb_config")
cfg.damping = opts.damping or 0.5
cfg.mix = opts.mix or 0.3
cfg.room = opts.room or 0.5
return C.pxl8_sfx_reverb_create(cfg)
end
function sfx.reverb_set_damping(node, damping)
C.pxl8_sfx_reverb_set_damping(node, damping)
end
function sfx.reverb_set_mix(node, mix)
C.pxl8_sfx_reverb_set_mix(node, mix)
end
function sfx.reverb_set_room(node, room)
C.pxl8_sfx_reverb_set_room(node, room)
end
function sfx.set_master_volume(volume)
C.pxl8_sfx_mixer_set_master_volume(core.sfx, volume)
end
function sfx.stop_all(ctx)
C.pxl8_sfx_stop_all(ctx)
end
function sfx.stop_voice(ctx, voice_id)
C.pxl8_sfx_stop_voice(ctx, voice_id)
end
function sfx.voice_params(opts)
opts = opts or {}
local params = ffi.new("pxl8_sfx_voice_params")
params.amp_env.attack = opts.attack or 0.01
params.amp_env.decay = opts.decay or 0.1
params.amp_env.sustain = opts.sustain or 0.5
params.amp_env.release = opts.release or 0.2
params.filter_env.attack = opts.filter_attack or 0.01
params.filter_env.decay = opts.filter_decay or 0.1
params.filter_env.sustain = opts.filter_sustain or 0.3
params.filter_env.release = opts.filter_release or 0.1
params.filter_type = opts.filter_type or C.PXL8_SFX_FILTER_NONE
params.lfo_target = opts.lfo_target or C.PXL8_SFX_LFO_PITCH
params.lfo_waveform = opts.lfo_waveform or C.PXL8_SFX_WAVE_SINE
params.waveform = opts.waveform or C.PXL8_SFX_WAVE_SINE
params.filter_cutoff = opts.filter_cutoff or 4000
params.filter_env_depth = opts.filter_env_depth or 0
params.filter_resonance = opts.filter_resonance or 0
params.fx_send = opts.fx_send or 0
params.lfo_depth = opts.lfo_depth or 0
params.lfo_rate = opts.lfo_rate or 0
params.pulse_width = opts.pulse_width or 0.5
return params
end
return sfx

View file

@ -1,82 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local tilemap = {}
tilemap.TILE_FLIP_X = 1
tilemap.TILE_FLIP_Y = 2
tilemap.TILE_SOLID = 4
tilemap.TILE_TRIGGER = 8
local tile_data = setmetatable({}, {__mode = "k"})
function tilemap.tilesheet_new(tile_size)
return C.pxl8_tilesheet_create(tile_size or 16)
end
function tilemap.tilesheet_destroy(tilesheet)
C.pxl8_tilesheet_destroy(tilesheet)
end
function tilemap.tilesheet_load(tilesheet, filepath)
return C.pxl8_tilesheet_load(tilesheet, filepath, core.gfx)
end
function tilemap.new(width, height, tile_size)
return C.pxl8_tilemap_create(width, height, tile_size or 16)
end
function tilemap.destroy(tm)
C.pxl8_tilemap_destroy(tm)
end
function tilemap.set_tilesheet(tm, tilesheet)
return C.pxl8_tilemap_set_tilesheet(tm, tilesheet)
end
function tilemap.set_tile(tm, layer, x, y, tile_id, flags)
C.pxl8_tilemap_set_tile(tm, layer or 0, x, y, tile_id or 0, flags or 0)
end
function tilemap.get_tile_id(tm, layer, x, y)
return C.pxl8_tilemap_get_tile_id(tm, layer or 0, x, y)
end
function tilemap.set_camera(tm, x, y)
C.pxl8_tilemap_set_camera(tm, x, y)
end
function tilemap.render(tm)
C.pxl8_tilemap_render(tm, core.gfx)
end
function tilemap.render_layer(tm, layer)
C.pxl8_tilemap_render_layer(tm, core.gfx, layer)
end
function tilemap.is_solid(tm, x, y)
return C.pxl8_tilemap_is_solid(tm, x, y)
end
function tilemap.check_collision(tm, x, y, w, h)
return C.pxl8_tilemap_check_collision(tm, x, y, w, h)
end
function tilemap.get_tile_data(tm, tile_id)
if not tm or tile_id == 0 then return nil end
if not tile_data[tm] then return nil end
return tile_data[tm][tile_id]
end
function tilemap.load_ase(tm, filepath, layer)
return C.pxl8_tilemap_load_ase(tm, filepath, layer or 0)
end
function tilemap.set_tile_data(tm, tile_id, data)
if not tm or tile_id == 0 then return end
if not tile_data[tm] then tile_data[tm] = {} end
tile_data[tm][tile_id] = data
end
return tilemap

View file

@ -1,68 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local transition = {}
local transition_types = {
fade = 0,
wipe_left = 1,
wipe_right = 2,
wipe_up = 3,
wipe_down = 4,
circle_open = 5,
circle_close = 6,
dissolve = 7,
pixelate = 8
}
function transition.create(type_name, duration)
local type_id = transition_types[type_name] or 0
return C.pxl8_transition_create(type_id, duration or 1.0)
end
function transition.destroy(t)
C.pxl8_transition_destroy(t)
end
function transition.get_progress(t)
return C.pxl8_transition_get_progress(t)
end
function transition.is_active(t)
return C.pxl8_transition_is_active(t)
end
function transition.is_complete(t)
return C.pxl8_transition_is_complete(t)
end
function transition.render(t)
C.pxl8_transition_render(t, core.gfx)
end
function transition.reset(t)
C.pxl8_transition_reset(t)
end
function transition.set_color(t, color)
C.pxl8_transition_set_color(t, color)
end
function transition.set_reverse(t, reverse)
C.pxl8_transition_set_reverse(t, reverse)
end
function transition.start(t)
C.pxl8_transition_start(t)
end
function transition.stop(t)
C.pxl8_transition_stop(t)
end
function transition.update(t, dt)
C.pxl8_transition_update(t, dt)
end
return transition

View file

@ -1,65 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local vfx = {}
function vfx.raster_bars(bars, time)
local c_bars = ffi.new("pxl8_raster_bar[?]", #bars)
for i, bar in ipairs(bars) do
c_bars[i-1].base_y = bar.base_y or 0
c_bars[i-1].amplitude = bar.amplitude or 10
c_bars[i-1].height = bar.height or 5
c_bars[i-1].speed = bar.speed or 1.0
c_bars[i-1].phase = bar.phase or 0
c_bars[i-1].color = bar.color or 15
c_bars[i-1].fade_color = bar.fade_color or bar.color or 15
end
C.pxl8_vfx_raster_bars(core.gfx, c_bars, #bars, time)
end
function vfx.plasma(time, scale1, scale2, palette_offset)
C.pxl8_vfx_plasma(core.gfx, time, scale1 or 0.05, scale2 or 0.03, palette_offset or 0)
end
function vfx.rotozoom(angle, zoom, cx, cy)
local width = C.pxl8_gfx_get_width(core.gfx)
local height = C.pxl8_gfx_get_height(core.gfx)
C.pxl8_vfx_rotozoom(core.gfx, angle, zoom, cx or width/2, cy or height/2)
end
function vfx.tunnel(time, speed, twist)
C.pxl8_vfx_tunnel(core.gfx, time, speed or 2.0, twist or 0.5)
end
function vfx.explosion(ps, x, y, color, force)
C.pxl8_vfx_explosion(ps, x, y, color or 15, force or 200.0)
end
function vfx.fire(ps, x, y, width, palette_start)
C.pxl8_vfx_fire(ps, x, y, width or 50, palette_start or 64)
end
function vfx.rain(ps, width, wind)
local w = width or C.pxl8_gfx_get_width(core.gfx)
C.pxl8_vfx_rain(ps, w, wind or 0.0)
end
function vfx.smoke(ps, x, y, color)
C.pxl8_vfx_smoke(ps, x, y, color or 8)
end
function vfx.snow(ps, width, wind)
local w = width or C.pxl8_gfx_get_width(core.gfx)
C.pxl8_vfx_snow(ps, w, wind or 10.0)
end
function vfx.sparks(ps, x, y, color)
C.pxl8_vfx_sparks(ps, x, y, color or 15)
end
function vfx.starfield(ps, speed, spread)
C.pxl8_vfx_starfield(ps, speed or 5.0, spread or 500.0)
end
return vfx

View file

@ -1,110 +0,0 @@
local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")
local world = {}
world.PROCGEN_ROOMS = C.PXL8_PROCGEN_ROOMS
world.PROCGEN_TERRAIN = C.PXL8_PROCGEN_TERRAIN
function world.new()
return C.pxl8_world_create()
end
function world.destroy(w)
C.pxl8_world_destroy(w)
end
function world.load(w, filepath)
return C.pxl8_world_load(w, filepath)
end
function world.render(w, camera_pos)
local vec = ffi.new("pxl8_vec3", {x = camera_pos[1], y = camera_pos[2], z = camera_pos[3]})
C.pxl8_world_render(w, core.gfx, vec)
end
function world.unload(w)
C.pxl8_world_unload(w)
end
function world.is_loaded(w)
return C.pxl8_world_is_loaded(w)
end
function world.generate(w, params)
local c_params = ffi.new("pxl8_procgen_params")
c_params.type = params.type or C.PXL8_PROCGEN_ROOMS
c_params.width = params.width or 32
c_params.height = params.height or 32
c_params.depth = params.depth or 0
c_params.seed = params.seed or 0
c_params.min_room_size = params.min_room_size or 5
c_params.max_room_size = params.max_room_size or 10
c_params.num_rooms = params.num_rooms or 8
return C.pxl8_world_generate(w, core.gfx, c_params)
end
function world.procgen_tex(params)
local width = params.width or 64
local height = params.height or 64
local buffer = ffi.new("u8[?]", width * height)
local tex_params = ffi.new("pxl8_procgen_tex_params")
local name = params.name or ""
ffi.copy(tex_params.name, name, math.min(#name, 15))
tex_params.seed = params.seed or 0
tex_params.width = width
tex_params.height = height
tex_params.scale = params.scale or 1.0
tex_params.roughness = params.roughness or 0.0
tex_params.base_color = params.base_color or 0
tex_params.variation = params.variation or 0
C.pxl8_procgen_tex(buffer, tex_params)
local tex_id = C.pxl8_gfx_create_texture(core.gfx, buffer, width, height)
if tex_id < 0 then
return nil
end
return tex_id
end
function world.apply_textures(w, texture_defs)
local count = #texture_defs
local textures = ffi.new("pxl8_world_texture[?]", count)
for i, def in ipairs(texture_defs) do
local idx = i - 1
ffi.copy(textures[idx].name, def.name or "", math.min(#(def.name or ""), 15))
textures[idx].texture_id = def.texture_id or 0
if def.rule then
textures[idx].rule = ffi.cast("bool (*)(const pxl8_vec3*, const pxl8_bsp_face*, const pxl8_bsp*)",
function(normal, face, bsp)
return def.rule(normal[0], face, bsp)
end)
else
textures[idx].rule = nil
end
end
local result = C.pxl8_world_apply_textures(w, textures, count)
return result
end
function world.check_collision(w, x, y, z, radius)
local pos = ffi.new("pxl8_vec3", {x = x, y = y, z = z})
return C.pxl8_world_check_collision(w, pos, radius)
end
function world.resolve_collision(w, from_x, from_y, from_z, to_x, to_y, to_z, radius)
local from = ffi.new("pxl8_vec3", {x = from_x, y = from_y, z = from_z})
local to = ffi.new("pxl8_vec3", {x = to_x, y = to_y, z = to_z})
local result = C.pxl8_world_resolve_collision(w, from, to, radius)
return result.x, result.y, result.z
end
return world

View file

@ -1,485 +0,0 @@
#define PXL8_AUTHORS "asrael <asrael@pxl8.org>"
#define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org"
#define PXL8_VERSION "0.1.0"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/stat.h>
#include <unistd.h>
#include "pxl8_game.h"
#include "pxl8_hal.h"
#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 {
pxl8_cart* cart;
pxl8_game* game;
pxl8_repl* repl;
pxl8_log log;
const pxl8_hal* hal;
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;
pxl8_log_init(&sys->log);
if (!hal) {
pxl8_error("hal cannot be null");
free(sys);
return NULL;
}
sys->hal = hal;
sys->game = (pxl8_game*)calloc(1, sizeof(pxl8_game));
if (!sys->game) {
pxl8_error("failed to allocate game");
free(sys);
return NULL;
}
return sys;
}
void pxl8_destroy(pxl8* sys) {
if (!sys) return;
if (sys->game) free(sys->game);
if (sys->cart) pxl8_cart_destroy(sys->cart);
if (sys->hal && sys->platform_data) sys->hal->destroy(sys->platform_data);
free(sys);
}
static void pxl8_print_help(void) {
printf("pxl8 %s - pixel art game framework\n", PXL8_VERSION);
printf("%s\n\n", PXL8_COPYRIGHT);
printf("Usage: pxl8 [path] [--repl]\n\n");
printf(" pxl8 Run main.fnl from current directory\n");
printf(" pxl8 ./game Run game from folder\n");
printf(" pxl8 game.pxc Run packed cart file\n");
printf(" pxl8 --repl Run with REPL enabled\n\n");
printf("Other commands:\n");
printf(" pxl8 pack <folder> <out.pxc> Pack folder into cart file\n");
printf(" pxl8 bundle <in> <out> Bundle cart into executable\n");
printf(" pxl8 help Show this help\n");
}
pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
if (!sys || !sys->game) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_game* game = sys->game;
const char* script_arg = NULL;
bool bundle_mode = false;
bool pack_mode = false;
bool run_mode = false;
const char* pack_input = NULL;
const char* pack_output = NULL;
bool has_embedded = pxl8_cart_has_embedded(argv[0]);
for (i32 i = 1; i < argc; i++) {
if (strcmp(argv[i], "help") == 0 || strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
pxl8_print_help();
return PXL8_ERROR_INVALID_ARGUMENT;
} else if (strcmp(argv[i], "run") == 0) {
run_mode = true;
} else if (strcmp(argv[i], "--repl") == 0) {
game->repl_mode = true;
} else if (strcmp(argv[i], "bundle") == 0 || strcmp(argv[i], "--bundle") == 0) {
bundle_mode = true;
if (i + 2 < argc) {
pack_input = argv[++i];
pack_output = argv[++i];
} else {
pxl8_error("bundle requires <folder|.pxc> <output>");
return PXL8_ERROR_INVALID_ARGUMENT;
}
} else if (strcmp(argv[i], "pack") == 0 || strcmp(argv[i], "--pack") == 0) {
pack_mode = true;
if (i + 2 < argc) {
pack_input = argv[++i];
pack_output = argv[++i];
} else {
pxl8_error("pack requires <folder> <output.pxc>");
return PXL8_ERROR_INVALID_ARGUMENT;
}
} else if (!script_arg) {
script_arg = argv[i];
}
}
if (!run_mode && !bundle_mode && !pack_mode) {
run_mode = true;
}
if (bundle_mode) {
char exe_path[1024];
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
if (len == -1) {
pxl8_error("failed to resolve executable path");
return PXL8_ERROR_SYSTEM_FAILURE;
}
exe_path[len] = '\0';
pxl8_result result = pxl8_cart_bundle(pack_input, pack_output, exe_path);
return result;
}
if (pack_mode) {
pxl8_result result = pxl8_cart_pack(pack_input, pack_output);
return result;
}
pxl8_info("Starting up");
game->script = pxl8_script_create(game->repl_mode);
if (!game->script) {
pxl8_error("failed to initialize scripting: %s", pxl8_script_get_last_error(game->script));
return PXL8_ERROR_INITIALIZATION_FAILED;
}
const char* cart_path = script_arg;
char* original_cwd = getcwd(NULL, 0);
bool load_embedded = has_embedded && !run_mode;
bool load_from_path = false;
if (!load_embedded && run_mode) {
if (!cart_path) {
if (access("main.fnl", F_OK) == 0 || access("main.lua", F_OK) == 0) {
cart_path = ".";
} else {
pxl8_error("no main.fnl or main.lua found in current directory");
free(original_cwd);
return PXL8_ERROR_INITIALIZATION_FAILED;
}
}
struct stat st;
load_from_path = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) ||
strstr(cart_path, ".pxc");
}
if (load_embedded || load_from_path) {
sys->cart = pxl8_cart_create();
pxl8_result load_result = load_embedded
? pxl8_cart_load_embedded(sys->cart, argv[0])
: pxl8_cart_load(sys->cart, cart_path);
if (!sys->cart || load_result != PXL8_OK) {
pxl8_error("failed to load cart%s%s", load_from_path ? ": " : "", load_from_path ? cart_path : "");
if (sys->cart) pxl8_cart_destroy(sys->cart);
sys->cart = NULL;
free(original_cwd);
return PXL8_ERROR_INITIALIZATION_FAILED;
}
pxl8_cart_mount(sys->cart);
pxl8_script_load_cart_manifest(game->script, sys->cart);
if (load_from_path) {
pxl8_script_set_cart_path(game->script, pxl8_cart_get_base_path(sys->cart), original_cwd);
}
pxl8_strncpy(game->script_path, "main.fnl", sizeof(game->script_path));
} else if (script_arg) {
pxl8_strncpy(game->script_path, script_arg, sizeof(game->script_path));
}
free(original_cwd);
const char* window_title = pxl8_cart_get_title(sys->cart);
if (!window_title) window_title = "pxl8";
pxl8_resolution resolution = pxl8_cart_get_resolution(sys->cart);
pxl8_pixel_mode pixel_mode = pxl8_cart_get_pixel_mode(sys->cart);
pxl8_size window_size = pxl8_cart_get_window_size(sys->cart);
pxl8_size render_size = pxl8_get_resolution_dimensions(resolution);
sys->platform_data = sys->hal->create(render_size.w, render_size.h, window_title, window_size.w, window_size.h);
if (!sys->platform_data) {
pxl8_error("failed to create platform context");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
game->gfx = pxl8_gfx_create(sys->hal, sys->platform_data, pixel_mode, resolution);
if (!game->gfx) {
pxl8_error("failed to create graphics context");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
if (pxl8_gfx_load_font_atlas(game->gfx) != PXL8_OK) {
pxl8_error("failed to load font atlas");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
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);
if (game->script_path[0] != '\0') {
pxl8_result result = pxl8_script_load_main(game->script, game->script_path);
game->script_loaded = (result == PXL8_OK);
if (game->script_loaded && !game->repl_mode) {
pxl8_result init_result = pxl8_script_call_function(game->script, "init");
if (init_result != PXL8_OK) {
pxl8_script_error("%s", pxl8_script_get_last_error(game->script));
}
}
}
game->last_time = sys->hal->get_ticks();
game->running = true;
return PXL8_OK;
}
pxl8_result pxl8_update(pxl8* sys) {
if (!sys || !sys->game) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_game* game = sys->game;
u64 current_time = sys->hal->get_ticks();
f32 dt = (f32)(current_time - game->last_time) / 1000000000.0f;
game->last_time = current_time;
game->time += dt;
game->fps_accumulator += dt;
game->fps_frame_count++;
if (game->fps_accumulator >= 1.0f) {
game->fps = (f32)game->fps_frame_count / game->fps_accumulator;
game->fps_accumulator = 0.0f;
game->fps_frame_count = 0;
}
#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) {
pxl8_script_call_function(game->script, "init");
}
if (pxl8_script_load_module(game->script, "pxl8") != PXL8_OK) {
const char* err_msg = pxl8_script_get_last_error(game->script);
pxl8_error("failed to setup pxl8 global: %s", err_msg);
}
sys->repl = pxl8_repl_create();
game->repl_started = true;
}
if (game->repl_mode && sys->repl) {
if (pxl8_repl_should_quit(sys->repl)) game->running = false;
pxl8_repl_command* cmd = pxl8_repl_pop_command(sys->repl);
if (cmd) {
pxl8_result result = pxl8_script_eval_repl(game->script, pxl8_repl_command_buffer(cmd));
if (result != PXL8_OK) {
if (pxl8_script_is_incomplete_input(game->script)) {
pxl8_repl_signal_complete(sys->repl);
} else {
pxl8_error("%s", pxl8_script_get_last_error(game->script));
pxl8_repl_clear_accumulator(sys->repl);
pxl8_repl_signal_complete(sys->repl);
}
} else {
pxl8_repl_clear_accumulator(sys->repl);
pxl8_repl_signal_complete(sys->repl);
}
}
}
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);
}
return PXL8_OK;
}
pxl8_result pxl8_frame(pxl8* sys) {
if (!sys || !sys->game) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_game* game = sys->game;
pxl8_bounds bounds = pxl8_gfx_get_bounds(game->gfx);
if (game->script_loaded) {
pxl8_result frame_result = pxl8_script_call_function(game->script, "frame");
if (frame_result == PXL8_ERROR_SCRIPT_ERROR) {
pxl8_error("error calling frame: %s", pxl8_script_get_last_error(game->script));
}
} else {
pxl8_clear(game->gfx, 32);
i32 render_w = pxl8_gfx_get_width(game->gfx);
i32 render_h = pxl8_gfx_get_height(game->gfx);
for (i32 y = 0; y < render_h; y += 24) {
for (i32 x = 0; x < render_w; x += 32) {
u32 color = ((x / 32) + (y / 24) + (i32)(game->time * 2)) % 8;
pxl8_rect_fill(game->gfx, x, y, 31, 23, color);
}
}
}
pxl8_gfx_set_viewport(game->gfx, pxl8_gfx_viewport(bounds, pxl8_gfx_get_width(game->gfx), pxl8_gfx_get_height(game->gfx)));
pxl8_gfx_upload_framebuffer(game->gfx);
pxl8_gfx_present(game->gfx);
memset(game->input.keys_pressed, 0, sizeof(game->input.keys_pressed));
memset(game->input.keys_released, 0, sizeof(game->input.keys_released));
memset(game->input.mouse_buttons_pressed, 0, sizeof(game->input.mouse_buttons_pressed));
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;
game->input.mouse_wheel_y = 0;
return PXL8_OK;
}
void pxl8_quit(pxl8* sys) {
if (!sys || !sys->game) return;
pxl8_game* game = sys->game;
pxl8_info("Shutting down");
if (sys->cart) {
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);
}
bool pxl8_is_running(const pxl8* sys) {
return sys && sys->game && sys->game->running;
}
void pxl8_set_running(pxl8* sys, bool running) {
if (sys && sys->game) {
sys->game->running = running;
if (!running && sys->repl) {
pxl8_repl_destroy(sys->repl);
sys->repl = NULL;
}
}
}
f32 pxl8_get_fps(const pxl8* sys) {
return (sys && sys->game) ? sys->game->fps : 0.0f;
}
pxl8_gfx* pxl8_get_gfx(const pxl8* sys) {
return (sys && sys->game) ? sys->game->gfx : NULL;
}
pxl8_input_state* pxl8_get_input(const pxl8* sys) {
return (sys && sys->game) ? &sys->game->input : NULL;
}
pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys) {
return (sys && sys->game) ? sys->game->mixer : NULL;
}
void pxl8_center_cursor(pxl8* sys) {
if (!sys || !sys->hal || !sys->hal->center_cursor) return;
sys->hal->center_cursor(sys->platform_data);
}
void pxl8_set_cursor(pxl8* sys, pxl8_cursor cursor) {
if (!sys || !sys->hal || !sys->hal->set_cursor) return;
sys->hal->set_cursor(sys->platform_data, cursor);
}
void pxl8_set_relative_mouse_mode(pxl8* sys, bool enabled) {
if (!sys || !sys->hal || !sys->hal->set_relative_mouse_mode) return;
sys->hal->set_relative_mouse_mode(sys->platform_data, enabled);
if (sys->game) {
sys->game->input.mouse_relative_mode = enabled;
}
}
pxl8_size pxl8_get_resolution_dimensions(pxl8_resolution resolution) {
switch (resolution) {
case PXL8_RESOLUTION_240x160: return (pxl8_size){240, 160};
case PXL8_RESOLUTION_320x180: return (pxl8_size){320, 180};
case PXL8_RESOLUTION_320x240: return (pxl8_size){320, 240};
case PXL8_RESOLUTION_640x360: return (pxl8_size){640, 360};
case PXL8_RESOLUTION_640x480: return (pxl8_size){640, 480};
case PXL8_RESOLUTION_800x600: return (pxl8_size){800, 600};
case PXL8_RESOLUTION_960x540: return (pxl8_size){960, 540};
default: return (pxl8_size){640, 360};
}
}

View file

@ -1,431 +0,0 @@
#include "pxl8_anim.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_ase.h"
#include "pxl8_atlas.h"
#include "pxl8_log.h"
#define PXL8_ANIM_MAX_STATES 32
typedef struct pxl8_anim_state {
char* name;
pxl8_anim* anim;
} pxl8_anim_state;
typedef struct pxl8_anim_state_machine {
pxl8_anim_state states[PXL8_ANIM_MAX_STATES];
u16 state_count;
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");
return NULL;
}
pxl8_anim* anim = (pxl8_anim*)calloc(1, sizeof(pxl8_anim));
if (!anim) {
pxl8_error("Failed to allocate animation");
return NULL;
}
anim->frame_ids = (u32*)malloc(frame_count * sizeof(u32));
if (!anim->frame_ids) {
pxl8_error("Failed to allocate frame IDs");
free(anim);
return NULL;
}
memcpy(anim->frame_ids, frame_ids, frame_count * sizeof(u32));
if (frame_durations) {
anim->frame_durations = (u16*)malloc(frame_count * sizeof(u16));
if (!anim->frame_durations) {
pxl8_error("Failed to allocate frame durations");
free(anim->frame_ids);
free(anim);
return NULL;
}
memcpy(anim->frame_durations, frame_durations, frame_count * sizeof(u16));
} else {
anim->frame_durations = (u16*)calloc(frame_count, sizeof(u16));
if (!anim->frame_durations) {
pxl8_error("Failed to allocate frame durations");
free(anim->frame_ids);
free(anim);
return NULL;
}
for (u16 i = 0; i < frame_count; i++) {
anim->frame_durations[i] = 100;
}
}
anim->frame_count = frame_count;
anim->current_frame = 0;
anim->time_accumulator = 0.0f;
anim->loop = true;
anim->playing = true;
anim->reverse = false;
anim->speed = 1.0f;
anim->state_machine = NULL;
anim->on_complete = NULL;
anim->on_frame_change = NULL;
anim->userdata = NULL;
return anim;
}
pxl8_anim* pxl8_anim_create_from_ase(pxl8_gfx* gfx, const char* path) {
if (!gfx || !path) {
pxl8_error("Invalid parameters for ASE animation creation");
return NULL;
}
pxl8_ase_file ase_file;
pxl8_result result = pxl8_ase_load(path, &ase_file);
if (result != PXL8_OK) {
pxl8_error("Failed to load ASE file: %s", path);
return NULL;
}
if (ase_file.frame_count == 0) {
pxl8_error("ASE file has no frames: %s", path);
pxl8_ase_destroy(&ase_file);
return NULL;
}
u32* frame_ids = (u32*)malloc(ase_file.frame_count * sizeof(u32));
u16* frame_durations = (u16*)malloc(ase_file.frame_count * sizeof(u16));
if (!frame_ids || !frame_durations) {
pxl8_error("Failed to allocate frame arrays");
free(frame_ids);
free(frame_durations);
pxl8_ase_destroy(&ase_file);
return NULL;
}
for (u32 i = 0; i < ase_file.frame_count; i++) {
pxl8_ase_frame* frame = &ase_file.frames[i];
result = pxl8_gfx_create_texture(gfx, frame->pixels, frame->width, frame->height);
if (result != PXL8_OK) {
pxl8_error("Failed to create texture for frame %u", i);
free(frame_ids);
free(frame_durations);
pxl8_ase_destroy(&ase_file);
return NULL;
}
frame_ids[i] = i;
frame_durations[i] = frame->duration;
}
pxl8_anim* anim = pxl8_anim_create(frame_ids, frame_durations, ase_file.frame_count);
free(frame_ids);
free(frame_durations);
pxl8_ase_destroy(&ase_file);
return anim;
}
void pxl8_anim_destroy(pxl8_anim* anim) {
if (!anim) return;
if (anim->state_machine) {
for (u16 i = 0; i < anim->state_machine->state_count; i++) {
free(anim->state_machine->states[i].name);
pxl8_anim_destroy(anim->state_machine->states[i].anim);
}
free(anim->state_machine);
}
free(anim->frame_ids);
free(anim->frame_durations);
free(anim);
}
pxl8_result pxl8_anim_add_state(pxl8_anim* anim, const char* name, pxl8_anim* state_anim) {
if (!anim || !name || !state_anim) {
return PXL8_ERROR_NULL_POINTER;
}
if (!anim->state_machine) {
anim->state_machine = (pxl8_anim_state_machine*)calloc(1, sizeof(pxl8_anim_state_machine));
if (!anim->state_machine) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
}
if (anim->state_machine->state_count >= PXL8_ANIM_MAX_STATES) {
pxl8_error("Cannot add more states, maximum %d reached", PXL8_ANIM_MAX_STATES);
return PXL8_ERROR_OUT_OF_MEMORY;
}
u16 idx = anim->state_machine->state_count;
anim->state_machine->states[idx].name = strdup(name);
if (!anim->state_machine->states[idx].name) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
anim->state_machine->states[idx].anim = state_anim;
anim->state_machine->state_count++;
return PXL8_OK;
}
u16 pxl8_anim_get_current_frame(const pxl8_anim* anim) {
if (!anim) return 0;
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->current_frame;
}
}
return anim->current_frame;
}
u32 pxl8_anim_get_current_frame_id(const pxl8_anim* anim) {
if (!anim || !anim->frame_ids) return 0;
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->frame_ids) {
return state_anim->frame_ids[state_anim->current_frame];
}
}
return anim->frame_ids[anim->current_frame];
}
const char* pxl8_anim_get_state(const pxl8_anim* anim) {
if (!anim || !anim->state_machine) return NULL;
if (anim->state_machine->current_state < anim->state_machine->state_count) {
return anim->state_machine->states[anim->state_machine->current_state].name;
}
return NULL;
}
bool pxl8_anim_has_state_machine(const pxl8_anim* anim) {
return anim && anim->state_machine && anim->state_machine->state_count > 0;
}
bool pxl8_anim_is_complete(const pxl8_anim* anim) {
if (!anim) return true;
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 pxl8_anim_is_complete(state_anim);
}
}
if (anim->loop) return false;
if (anim->reverse) {
return anim->current_frame == 0;
} else {
return anim->current_frame >= anim->frame_count - 1;
}
}
bool pxl8_anim_is_playing(const pxl8_anim* anim) {
if (!anim) return false;
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->playing;
}
}
return anim->playing;
}
void pxl8_anim_pause(pxl8_anim* anim) {
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->playing = false;
return;
}
}
anim->playing = false;
}
void pxl8_anim_play(pxl8_anim* anim) {
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->playing = true;
return;
}
}
anim->playing = true;
}
void pxl8_anim_render_sprite(const pxl8_anim* anim, pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y) {
if (!anim || !gfx) return;
u32 sprite_id = pxl8_anim_get_current_frame_id(anim);
pxl8_sprite(gfx, sprite_id, x, y, w, h, flip_x, flip_y);
}
void pxl8_anim_reset(pxl8_anim* anim) {
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_reset(state_anim);
return;
}
}
anim->current_frame = anim->reverse ? anim->frame_count - 1 : 0;
anim->time_accumulator = 0.0f;
}
void pxl8_anim_set_frame(pxl8_anim* anim, u16 frame) {
if (!anim) return;
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;
pxl8_anim_get_active(anim)->loop = loop;
}
void pxl8_anim_set_reverse(pxl8_anim* anim, bool reverse) {
if (!anim) return;
pxl8_anim_get_active(anim)->reverse = reverse;
}
void pxl8_anim_set_speed(pxl8_anim* anim, f32 speed) {
if (!anim) return;
pxl8_anim_get_active(anim)->speed = speed;
}
pxl8_result pxl8_anim_set_state(pxl8_anim* anim, const char* name) {
if (!anim || !name) {
return PXL8_ERROR_NULL_POINTER;
}
if (!anim->state_machine) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
for (u16 i = 0; i < anim->state_machine->state_count; i++) {
if (strcmp(anim->state_machine->states[i].name, name) == 0) {
if (anim->state_machine->current_state != i) {
anim->state_machine->current_state = i;
pxl8_anim_reset(anim->state_machine->states[i].anim);
}
return PXL8_OK;
}
}
pxl8_error("Animation state not found: %s", name);
return PXL8_ERROR_INVALID_ARGUMENT;
}
void pxl8_anim_stop(pxl8_anim* anim) {
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_stop(state_anim);
return;
}
}
anim->playing = false;
pxl8_anim_reset(anim);
}
void pxl8_anim_update(pxl8_anim* anim, f32 dt) {
if (!anim || !anim->playing) 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_update(state_anim, dt);
return;
}
}
if (anim->frame_count == 0 || !anim->frame_durations) return;
anim->time_accumulator += dt * anim->speed * 1000.0f;
u16 current_duration = anim->frame_durations[anim->current_frame];
if (current_duration == 0) return;
while (anim->time_accumulator >= current_duration) {
anim->time_accumulator -= current_duration;
u16 old_frame = anim->current_frame;
if (anim->reverse) {
if (anim->current_frame > 0) {
anim->current_frame--;
} else {
if (anim->loop) {
anim->current_frame = anim->frame_count - 1;
} else {
anim->playing = false;
if (anim->on_complete) {
anim->on_complete(anim->userdata);
}
break;
}
}
} else {
if (anim->current_frame < anim->frame_count - 1) {
anim->current_frame++;
} else {
if (anim->loop) {
anim->current_frame = 0;
} else {
anim->playing = false;
if (anim->on_complete) {
anim->on_complete(anim->userdata);
}
break;
}
}
}
if (old_frame != anim->current_frame && anim->on_frame_change) {
anim->on_frame_change(anim->current_frame, anim->userdata);
}
current_duration = anim->frame_durations[anim->current_frame];
if (current_duration == 0) break;
}
}

View file

@ -1,56 +0,0 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_types.h"
typedef struct pxl8_anim_state_machine pxl8_anim_state_machine;
typedef struct pxl8_anim {
u32* frame_ids;
u16* frame_durations;
u16 frame_count;
u16 current_frame;
f32 time_accumulator;
bool loop;
bool playing;
bool reverse;
f32 speed;
pxl8_anim_state_machine* state_machine;
void (*on_complete)(void* userdata);
void (*on_frame_change)(u16 frame, void* userdata);
void* userdata;
} pxl8_anim;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_anim* pxl8_anim_create(const u32* frame_ids, const u16* frame_durations, u16 frame_count);
pxl8_anim* pxl8_anim_create_from_ase(pxl8_gfx* gfx, const char* path);
void pxl8_anim_destroy(pxl8_anim* anim);
pxl8_result pxl8_anim_add_state(pxl8_anim* anim, const char* name, pxl8_anim* state_anim);
u16 pxl8_anim_get_current_frame(const pxl8_anim* anim);
u32 pxl8_anim_get_current_frame_id(const pxl8_anim* anim);
const char* pxl8_anim_get_state(const pxl8_anim* anim);
bool pxl8_anim_has_state_machine(const pxl8_anim* anim);
bool pxl8_anim_is_complete(const pxl8_anim* anim);
bool pxl8_anim_is_playing(const pxl8_anim* anim);
void pxl8_anim_pause(pxl8_anim* anim);
void pxl8_anim_play(pxl8_anim* anim);
void pxl8_anim_render_sprite(const pxl8_anim* anim, pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y);
void pxl8_anim_reset(pxl8_anim* anim);
void pxl8_anim_set_frame(pxl8_anim* anim, u16 frame);
void pxl8_anim_set_loop(pxl8_anim* anim, bool loop);
void pxl8_anim_set_reverse(pxl8_anim* anim, bool reverse);
void pxl8_anim_set_speed(pxl8_anim* anim, f32 speed);
pxl8_result pxl8_anim_set_state(pxl8_anim* anim, const char* name);
void pxl8_anim_stop(pxl8_anim* anim);
void pxl8_anim_update(pxl8_anim* anim, f32 dt);
#ifdef __cplusplus
}
#endif

View file

@ -1,636 +0,0 @@
#include "pxl8_ase.h"
#include <stdlib.h>
#include <string.h>
#define MINIZ_NO_STDIO
#define MINIZ_NO_TIME
#define MINIZ_NO_ARCHIVE_APIS
#define MINIZ_NO_ARCHIVE_WRITING_APIS
#define MINIZ_NO_DEFLATE_APIS
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
#include <miniz.h>
#include "pxl8_io.h"
#include "pxl8_log.h"
static pxl8_result parse_ase_header(pxl8_stream* stream, pxl8_ase_header* header) {
header->file_size = pxl8_read_u32(stream);
header->magic = pxl8_read_u16(stream);
header->frames = pxl8_read_u16(stream);
header->width = pxl8_read_u16(stream);
header->height = pxl8_read_u16(stream);
header->color_depth = pxl8_read_u16(stream);
header->flags = pxl8_read_u32(stream);
header->speed = pxl8_read_u16(stream);
pxl8_skip_bytes(stream, 4);
header->transparent_index = pxl8_read_u32(stream);
header->n_colors = pxl8_read_u8(stream);
header->pixel_width = pxl8_read_u8(stream);
header->pixel_height = pxl8_read_u8(stream);
header->grid_x = pxl8_read_i16(stream);
header->grid_y = pxl8_read_i16(stream);
header->grid_width = pxl8_read_u16(stream);
header->grid_height = pxl8_read_u16(stream);
if (header->magic != PXL8_ASE_MAGIC) {
pxl8_error("Invalid ASE file magic: 0x%04X", header->magic);
return PXL8_ERROR_ASE_INVALID_MAGIC;
}
return PXL8_OK;
}
static pxl8_result parse_old_palette_chunk(pxl8_stream* stream, pxl8_ase_palette* palette) {
u16 packet_count = pxl8_read_u16(stream);
u32 total_colors = 0;
u32 temp_pos = pxl8_stream_position(stream);
for (u16 packet = 0; packet < packet_count; packet++) {
u8 skip_colors = pxl8_read_u8(stream);
u8 colors_in_packet = pxl8_read_u8(stream);
u32 actual_colors = (colors_in_packet == 0) ? 256 : colors_in_packet;
total_colors += skip_colors + actual_colors;
pxl8_skip_bytes(stream, actual_colors * 3);
}
palette->entry_count = total_colors;
palette->first_color = 0;
palette->last_color = total_colors - 1;
palette->colors = (u32*)malloc(total_colors * sizeof(u32));
if (!palette->colors) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
pxl8_stream_seek(stream, temp_pos);
u32 color_index = 0;
for (u16 packet = 0; packet < packet_count; packet++) {
u8 skip_colors = pxl8_read_u8(stream);
u8 colors_in_packet = pxl8_read_u8(stream);
u32 actual_colors = (colors_in_packet == 0) ? 256 : colors_in_packet;
for (u32 skip = 0; skip < skip_colors && color_index < total_colors; skip++) {
palette->colors[color_index++] = 0xFF000000;
}
for (u32 color = 0; color < actual_colors && color_index < total_colors; color++) {
u8 r = pxl8_read_u8(stream);
u8 g = pxl8_read_u8(stream);
u8 b = pxl8_read_u8(stream);
palette->colors[color_index++] = 0xFF000000 | (b << 16) | (g << 8) | r;
}
}
return PXL8_OK;
}
static pxl8_result parse_layer_chunk(pxl8_stream* stream, pxl8_ase_layer* layer) {
layer->flags = pxl8_read_u16(stream);
layer->layer_type = pxl8_read_u16(stream);
layer->child_level = pxl8_read_u16(stream);
pxl8_skip_bytes(stream, 4);
layer->blend_mode = pxl8_read_u16(stream);
layer->opacity = pxl8_read_u8(stream);
pxl8_skip_bytes(stream, 3);
u16 name_len = pxl8_read_u16(stream);
if (name_len > 0) {
layer->name = (char*)malloc(name_len + 1);
if (!layer->name) return PXL8_ERROR_OUT_OF_MEMORY;
pxl8_read_bytes(stream, layer->name, name_len);
layer->name[name_len] = '\0';
} else {
layer->name = NULL;
}
return PXL8_OK;
}
static pxl8_result parse_palette_chunk(pxl8_stream* stream, pxl8_ase_palette* palette) {
pxl8_skip_bytes(stream, 4);
palette->first_color = pxl8_read_u32(stream);
palette->last_color = pxl8_read_u32(stream);
pxl8_skip_bytes(stream, 8);
u32 color_count = palette->last_color - palette->first_color + 1;
if (color_count == 0 || color_count > 65536) {
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
}
palette->colors = (u32*)malloc(color_count * sizeof(u32));
if (!palette->colors) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
palette->entry_count = color_count;
for (u32 i = 0; i < color_count; i++) {
u16 flags = pxl8_read_u16(stream);
u8 r = pxl8_read_u8(stream);
u8 g = pxl8_read_u8(stream);
u8 b = pxl8_read_u8(stream);
u8 a = pxl8_read_u8(stream);
palette->colors[i] = (a << 24) | (b << 16) | (g << 8) | r;
if (flags & 1) {
u16 name_len = pxl8_read_u16(stream);
pxl8_skip_bytes(stream, name_len);
}
}
return PXL8_OK;
}
static pxl8_result parse_user_data_chunk(pxl8_stream* stream, pxl8_ase_user_data* user_data) {
u32 flags = pxl8_read_u32(stream);
user_data->has_text = (flags & 1) != 0;
user_data->has_color = (flags & 2) != 0;
if (user_data->has_text) {
u16 text_len = pxl8_read_u16(stream);
if (text_len > 0) {
user_data->text = (char*)malloc(text_len + 1);
if (!user_data->text) return PXL8_ERROR_OUT_OF_MEMORY;
pxl8_read_bytes(stream, user_data->text, text_len);
user_data->text[text_len] = '\0';
}
}
if (user_data->has_color) {
u8 r = pxl8_read_u8(stream);
u8 g = pxl8_read_u8(stream);
u8 b = pxl8_read_u8(stream);
u8 a = pxl8_read_u8(stream);
user_data->color = (a << 24) | (b << 16) | (g << 8) | r;
}
if (flags & 4) {
pxl8_skip_bytes(stream, 4);
u32 num_properties = pxl8_read_u32(stream);
if (num_properties > 0) {
user_data->properties = (pxl8_ase_property*)calloc(num_properties, sizeof(pxl8_ase_property));
if (!user_data->properties) return PXL8_ERROR_OUT_OF_MEMORY;
user_data->property_count = num_properties;
for (u32 i = 0; i < num_properties; i++) {
u16 name_len = pxl8_read_u16(stream);
if (name_len > 0) {
user_data->properties[i].name = (char*)malloc(name_len + 1);
if (!user_data->properties[i].name) return PXL8_ERROR_OUT_OF_MEMORY;
pxl8_read_bytes(stream, user_data->properties[i].name, name_len);
user_data->properties[i].name[name_len] = '\0';
}
u16 type = pxl8_read_u16(stream);
user_data->properties[i].type = type;
switch (type) {
case 1:
user_data->properties[i].bool_val = pxl8_read_u8(stream) != 0;
break;
case 2:
case 3:
case 4:
case 5:
user_data->properties[i].int_val = pxl8_read_i32(stream);
break;
case 6:
case 7:
user_data->properties[i].float_val = pxl8_read_f32(stream);
break;
case 8: {
u16 str_len = pxl8_read_u16(stream);
if (str_len > 0) {
user_data->properties[i].string_val = (char*)malloc(str_len + 1);
if (!user_data->properties[i].string_val) return PXL8_ERROR_OUT_OF_MEMORY;
pxl8_read_bytes(stream, user_data->properties[i].string_val, str_len);
user_data->properties[i].string_val[str_len] = '\0';
}
break;
}
default:
pxl8_skip_bytes(stream, 4);
break;
}
}
}
}
return PXL8_OK;
}
static pxl8_result parse_tileset_chunk(pxl8_stream* stream, pxl8_ase_tileset* tileset) {
tileset->id = pxl8_read_u32(stream);
tileset->flags = pxl8_read_u32(stream);
tileset->tile_count = pxl8_read_u32(stream);
tileset->tile_width = pxl8_read_u16(stream);
tileset->tile_height = pxl8_read_u16(stream);
tileset->base_index = pxl8_read_i16(stream);
pxl8_skip_bytes(stream, 14);
u16 name_len = pxl8_read_u16(stream);
if (name_len > 0) {
tileset->name = (char*)malloc(name_len + 1);
if (!tileset->name) return PXL8_ERROR_OUT_OF_MEMORY;
pxl8_read_bytes(stream, tileset->name, name_len);
tileset->name[name_len] = '\0';
}
if (tileset->flags & 1) {
pxl8_skip_bytes(stream, 8); // external_file_id + tileset_id
}
if (tileset->flags & 2) {
u32 compressed_size = pxl8_read_u32(stream);
tileset->pixels_size = tileset->tile_width * tileset->tile_height * tileset->tile_count;
tileset->pixels = (u8*)malloc(tileset->pixels_size);
if (!tileset->pixels) return PXL8_ERROR_OUT_OF_MEMORY;
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
mz_ulong dest_len = tileset->pixels_size;
i32 result = mz_uncompress(tileset->pixels, &dest_len, compressed_data, compressed_size);
if (result != MZ_OK) {
pxl8_error("Failed to decompress tileset data: miniz error %d", result);
free(tileset->pixels);
tileset->pixels = NULL;
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
}
}
tileset->tile_user_data = (pxl8_ase_user_data*)calloc(tileset->tile_count, sizeof(pxl8_ase_user_data));
if (!tileset->tile_user_data) return PXL8_ERROR_OUT_OF_MEMORY;
return PXL8_OK;
}
static pxl8_result parse_cel_chunk(pxl8_stream* stream, u32 chunk_size, pxl8_ase_cel* cel) {
if (chunk_size < 9) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
cel->layer_index = pxl8_read_u16(stream);
cel->x = pxl8_read_i16(stream);
cel->y = pxl8_read_i16(stream);
cel->opacity = pxl8_read_u8(stream);
cel->cel_type = pxl8_read_u16(stream);
pxl8_skip_bytes(stream, 7);
if (cel->cel_type == 2) {
if (chunk_size < 20) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
cel->image.width = pxl8_read_u16(stream);
cel->image.height = pxl8_read_u16(stream);
u32 pixels_size = cel->image.width * cel->image.height;
u32 compressed_size = chunk_size - 20;
cel->image.pixels = (u8*)malloc(pixels_size);
if (!cel->image.pixels) return PXL8_ERROR_OUT_OF_MEMORY;
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
mz_ulong dest_len = pixels_size;
i32 result = mz_uncompress(cel->image.pixels, &dest_len, compressed_data, compressed_size);
if (result != MZ_OK) {
pxl8_error("Failed to decompress cel data: miniz error %d", result);
free(cel->image.pixels);
cel->image.pixels = NULL;
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
}
if (dest_len != pixels_size) {
pxl8_warn("Decompressed size mismatch: expected %u, got %lu", pixels_size, dest_len);
}
} else if (cel->cel_type == 3) {
if (chunk_size < 36) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
cel->tilemap.width = pxl8_read_u16(stream);
cel->tilemap.height = pxl8_read_u16(stream);
cel->tilemap.bits_per_tile = pxl8_read_u16(stream);
cel->tilemap.tile_id_mask = pxl8_read_u32(stream);
cel->tilemap.x_flip_mask = pxl8_read_u32(stream);
cel->tilemap.y_flip_mask = pxl8_read_u32(stream);
cel->tilemap.diag_flip_mask = pxl8_read_u32(stream);
pxl8_skip_bytes(stream, 10);
u32 tile_count = cel->tilemap.width * cel->tilemap.height;
u32 bytes_per_tile = (cel->tilemap.bits_per_tile + 7) / 8;
u32 uncompressed_size = tile_count * bytes_per_tile;
u32 compressed_size = chunk_size - 36;
u8* temp_buffer = (u8*)malloc(uncompressed_size);
if (!temp_buffer) return PXL8_ERROR_OUT_OF_MEMORY;
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
mz_ulong dest_len = uncompressed_size;
i32 result = mz_uncompress(temp_buffer, &dest_len, compressed_data, compressed_size);
if (result != MZ_OK) {
pxl8_error("Failed to decompress tilemap data: miniz error %d", result);
free(temp_buffer);
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
}
cel->tilemap.tiles = (u32*)calloc(tile_count, sizeof(u32));
if (!cel->tilemap.tiles) {
free(temp_buffer);
return PXL8_ERROR_OUT_OF_MEMORY;
}
for (u32 i = 0; i < tile_count; i++) {
if (cel->tilemap.bits_per_tile == 32) {
cel->tilemap.tiles[i] = ((u32)temp_buffer[i * 4 + 0]) |
((u32)temp_buffer[i * 4 + 1] << 8) |
((u32)temp_buffer[i * 4 + 2] << 16) |
((u32)temp_buffer[i * 4 + 3] << 24);
} else if (cel->tilemap.bits_per_tile == 16) {
cel->tilemap.tiles[i] = ((u32)temp_buffer[i * 2 + 0]) |
((u32)temp_buffer[i * 2 + 1] << 8);
} else if (cel->tilemap.bits_per_tile == 8) {
cel->tilemap.tiles[i] = temp_buffer[i];
}
}
free(temp_buffer);
}
return PXL8_OK;
}
pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
if (!filepath || !ase_file) {
return PXL8_ERROR_NULL_POINTER;
}
memset(ase_file, 0, sizeof(pxl8_ase_file));
u8* file_data;
size_t file_size;
pxl8_result result = pxl8_io_read_binary_file(filepath, &file_data, &file_size);
if (result != PXL8_OK) {
return result;
}
if (file_size < 128) {
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_ASE_TRUNCATED_FILE;
}
pxl8_stream stream = pxl8_stream_create(file_data, (u32)file_size);
result = parse_ase_header(&stream, &ase_file->header);
if (result != PXL8_OK) {
pxl8_io_free_binary_data(file_data);
return result;
}
ase_file->frame_count = ase_file->header.frames;
ase_file->frames = (pxl8_ase_frame*)calloc(ase_file->frame_count, sizeof(pxl8_ase_frame));
if (!ase_file->frames) {
pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_OUT_OF_MEMORY;
}
pxl8_stream_seek(&stream, 128);
for (u16 frame_idx = 0; frame_idx < ase_file->header.frames; frame_idx++) {
u32 frame_start = pxl8_stream_position(&stream);
pxl8_ase_frame_header frame_header;
frame_header.frame_bytes = pxl8_read_u32(&stream);
frame_header.magic = pxl8_read_u16(&stream);
u16 old_chunks = pxl8_read_u16(&stream);
frame_header.duration = pxl8_read_u16(&stream);
pxl8_skip_bytes(&stream, 2);
if (frame_header.magic != PXL8_ASE_FRAME_MAGIC) {
pxl8_error("Invalid frame magic: 0x%04X", frame_header.magic);
result = PXL8_ERROR_ASE_INVALID_FRAME_MAGIC;
break;
}
if (old_chunks == 0xFFFF || old_chunks == 0xFFFE) {
frame_header.chunks = pxl8_read_u32(&stream);
} else {
frame_header.chunks = old_chunks;
pxl8_skip_bytes(&stream, 4);
}
pxl8_ase_frame* frame = &ase_file->frames[frame_idx];
frame->frame_id = frame_idx;
frame->width = ase_file->header.width;
frame->height = ase_file->header.height;
frame->duration = frame_header.duration;
u32 pixel_count = frame->width * frame->height;
frame->pixels = (u8*)calloc(pixel_count, sizeof(u8));
if (!frame->pixels) {
result = PXL8_ERROR_OUT_OF_MEMORY;
break;
}
u32 last_tileset_idx = 0xFFFFFFFF;
u32 tileset_tile_idx = 0;
for (u16 chunk_idx = 0; chunk_idx < frame_header.chunks; chunk_idx++) {
u32 chunk_start = pxl8_stream_position(&stream);
pxl8_ase_chunk_header chunk_header;
chunk_header.chunk_size = pxl8_read_u32(&stream);
chunk_header.chunk_type = pxl8_read_u16(&stream);
if (chunk_header.chunk_size < 6 || chunk_header.chunk_size > frame_header.frame_bytes) {
pxl8_error("Invalid chunk size %u in ASE file (frame_bytes=%u)",
chunk_header.chunk_size, frame_header.frame_bytes);
result = PXL8_ERROR_ASE_MALFORMED_CHUNK;
break;
}
switch (chunk_header.chunk_type) {
case PXL8_ASE_CHUNK_OLD_PALETTE:
if (!ase_file->palette.colors) {
result = parse_old_palette_chunk(&stream, &ase_file->palette);
}
break;
case PXL8_ASE_CHUNK_LAYER: {
pxl8_ase_layer* new_layers =
(pxl8_ase_layer*)realloc(ase_file->layers,
(ase_file->layer_count + 1) * sizeof(pxl8_ase_layer));
if (!new_layers) {
result = PXL8_ERROR_OUT_OF_MEMORY;
break;
}
ase_file->layers = new_layers;
result = parse_layer_chunk(&stream, &ase_file->layers[ase_file->layer_count]);
if (result == PXL8_OK) {
ase_file->layer_count++;
}
break;
}
case PXL8_ASE_CHUNK_CEL: {
pxl8_ase_cel cel = {0};
result = parse_cel_chunk(&stream, chunk_header.chunk_size - 6, &cel);
if (result == PXL8_OK) {
pxl8_ase_cel* new_cels = (pxl8_ase_cel*)realloc(frame->cels, (frame->cel_count + 1) * sizeof(pxl8_ase_cel));
if (!new_cels) {
result = PXL8_ERROR_OUT_OF_MEMORY;
break;
}
frame->cels = new_cels;
frame->cels[frame->cel_count] = cel;
frame->cel_count++;
if (cel.cel_type == 2 && cel.image.pixels) {
u32 copy_width = (cel.image.width < frame->width) ? cel.image.width : frame->width;
u32 copy_height = (cel.image.height < frame->height) ? cel.image.height : frame->height;
for (u32 y = 0; y < copy_height; y++) {
u32 src_offset = y * cel.image.width;
u32 dst_offset = (y + cel.y) * frame->width + cel.x;
if (dst_offset + copy_width <= pixel_count) {
for (u32 x = 0; x < copy_width; x++) {
u8 src_pixel = cel.image.pixels[src_offset + x];
bool is_transparent = false;
if (src_pixel < ase_file->palette.entry_count && ase_file->palette.colors) {
u32 color = ase_file->palette.colors[src_pixel];
is_transparent = ((color >> 24) & 0xFF) == 0;
}
if (!is_transparent) {
frame->pixels[dst_offset + x] = src_pixel;
}
}
}
}
}
}
break;
}
case PXL8_ASE_CHUNK_PALETTE:
if (ase_file->palette.colors) {
free(ase_file->palette.colors);
ase_file->palette.colors = NULL;
}
result = parse_palette_chunk(&stream, &ase_file->palette);
break;
case PXL8_ASE_CHUNK_TILESET: {
pxl8_ase_tileset* new_tilesets = (pxl8_ase_tileset*)realloc(ase_file->tilesets,
(ase_file->tileset_count + 1) * sizeof(pxl8_ase_tileset));
if (!new_tilesets) {
result = PXL8_ERROR_OUT_OF_MEMORY;
break;
}
ase_file->tilesets = new_tilesets;
memset(&ase_file->tilesets[ase_file->tileset_count], 0, sizeof(pxl8_ase_tileset));
result = parse_tileset_chunk(&stream, &ase_file->tilesets[ase_file->tileset_count]);
if (result == PXL8_OK) {
last_tileset_idx = ase_file->tileset_count;
tileset_tile_idx = 0;
ase_file->tileset_count++;
}
break;
}
case PXL8_ASE_CHUNK_USER_DATA: {
if (last_tileset_idx != 0xFFFFFFFF) {
pxl8_ase_tileset* tileset = &ase_file->tilesets[last_tileset_idx];
if (tileset_tile_idx < tileset->tile_count) {
result = parse_user_data_chunk(&stream, &tileset->tile_user_data[tileset_tile_idx]);
tileset_tile_idx++;
}
}
break;
}
default:
break;
}
if (result != PXL8_OK) break;
pxl8_stream_seek(&stream, chunk_start + chunk_header.chunk_size);
}
if (result != PXL8_OK) break;
pxl8_stream_seek(&stream, frame_start + frame_header.frame_bytes);
}
pxl8_io_free_binary_data(file_data);
if (result != PXL8_OK) {
pxl8_ase_destroy(ase_file);
}
return result;
}
void pxl8_ase_destroy(pxl8_ase_file* ase_file) {
if (!ase_file) return;
if (ase_file->frames) {
for (u32 i = 0; i < ase_file->frame_count; i++) {
if (ase_file->frames[i].pixels) free(ase_file->frames[i].pixels);
if (ase_file->frames[i].cels) {
for (u32 j = 0; j < ase_file->frames[i].cel_count; j++) {
pxl8_ase_cel* cel = &ase_file->frames[i].cels[j];
if (cel->cel_type == 2 && cel->image.pixels) {
free(cel->image.pixels);
} else if (cel->cel_type == 3 && cel->tilemap.tiles) {
free(cel->tilemap.tiles);
}
}
free(ase_file->frames[i].cels);
}
}
free(ase_file->frames);
}
if (ase_file->palette.colors) {
free(ase_file->palette.colors);
}
if (ase_file->layers) {
for (u32 i = 0; i < ase_file->layer_count; i++) {
if (ase_file->layers[i].name) {
free(ase_file->layers[i].name);
}
}
free(ase_file->layers);
}
if (ase_file->tilesets) {
for (u32 i = 0; i < ase_file->tileset_count; i++) {
if (ase_file->tilesets[i].name) free(ase_file->tilesets[i].name);
if (ase_file->tilesets[i].pixels) free(ase_file->tilesets[i].pixels);
if (ase_file->tilesets[i].tile_user_data) {
for (u32 j = 0; j < ase_file->tilesets[i].tile_count; j++) {
pxl8_ase_user_data* ud = &ase_file->tilesets[i].tile_user_data[j];
if (ud->text) free(ud->text);
if (ud->properties) {
for (u32 k = 0; k < ud->property_count; k++) {
if (ud->properties[k].name) free(ud->properties[k].name);
if (ud->properties[k].type == 8 && ud->properties[k].string_val) {
free(ud->properties[k].string_val);
}
}
free(ud->properties);
}
}
free(ase_file->tilesets[i].tile_user_data);
}
}
free(ase_file->tilesets);
}
memset(ase_file, 0, sizeof(pxl8_ase_file));
}

View file

@ -1,153 +0,0 @@
#pragma once
#include "pxl8_types.h"
#define PXL8_ASE_MAGIC 0xA5E0
#define PXL8_ASE_FRAME_MAGIC 0xF1FA
#define PXL8_ASE_CHUNK_CEL 0x2005
#define PXL8_ASE_CHUNK_LAYER 0x2004
#define PXL8_ASE_CHUNK_OLD_PALETTE 0x0004
#define PXL8_ASE_CHUNK_PALETTE 0x2019
#define PXL8_ASE_CHUNK_TILESET 0x2023
#define PXL8_ASE_CHUNK_USER_DATA 0x2020
typedef struct pxl8_ase_header {
u32 file_size;
u16 magic;
u16 frames;
u16 width;
u16 height;
u16 color_depth;
u32 flags;
u16 speed;
u32 transparent_index;
u8 n_colors;
u8 pixel_width;
u8 pixel_height;
i16 grid_x;
i16 grid_y;
u16 grid_width;
u16 grid_height;
} pxl8_ase_header;
typedef struct pxl8_ase_frame_header {
u32 frame_bytes;
u16 magic;
u16 chunks;
u16 duration;
} pxl8_ase_frame_header;
typedef struct pxl8_ase_chunk_header {
u32 chunk_size;
u16 chunk_type;
} pxl8_ase_chunk_header;
typedef struct pxl8_ase_cel {
u16 layer_index;
i16 x;
i16 y;
u8 opacity;
u16 cel_type;
union {
struct {
u16 width;
u16 height;
u8* pixels;
} image;
struct {
u16 width;
u16 height;
u16 bits_per_tile;
u32 tile_id_mask;
u32 x_flip_mask;
u32 y_flip_mask;
u32 diag_flip_mask;
u32* tiles;
} tilemap;
};
} pxl8_ase_cel;
typedef struct pxl8_ase_layer {
u16 flags;
u16 layer_type;
u16 child_level;
u16 blend_mode;
u8 opacity;
char* name;
} pxl8_ase_layer;
typedef struct pxl8_ase_palette {
u32 entry_count;
u32 first_color;
u32 last_color;
u32* colors;
} pxl8_ase_palette;
typedef struct pxl8_ase_frame {
u16 frame_id;
u16 width;
u16 height;
i16 x;
i16 y;
u16 duration;
u8* pixels;
u32 cel_count;
pxl8_ase_cel* cels;
} pxl8_ase_frame;
typedef struct pxl8_ase_property {
char* name;
u32 type;
union {
bool bool_val;
i32 int_val;
f32 float_val;
char* string_val;
};
} pxl8_ase_property;
typedef struct pxl8_ase_user_data {
char* text;
u32 color;
bool has_color;
bool has_text;
u32 property_count;
pxl8_ase_property* properties;
} pxl8_ase_user_data;
typedef struct pxl8_ase_tileset {
u32 id;
u32 flags;
u32 tile_count;
u16 tile_width;
u16 tile_height;
i16 base_index;
char* name;
u32 pixels_size;
u8* pixels;
pxl8_ase_user_data* tile_user_data;
} pxl8_ase_tileset;
typedef struct pxl8_ase_file {
u32 frame_count;
pxl8_ase_frame* frames;
pxl8_ase_header header;
u32 layer_count;
pxl8_ase_layer* layers;
pxl8_ase_palette palette;
u32 tileset_count;
pxl8_ase_tileset* tilesets;
} pxl8_ase_file;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file);
void pxl8_ase_destroy(pxl8_ase_file* ase_file);
#ifdef __cplusplus
}
#endif

View file

@ -1,392 +0,0 @@
#include "pxl8_atlas.h"
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pxl8_color.h"
#include "pxl8_log.h"
typedef struct pxl8_skyline_fit {
bool found;
u32 node_idx;
pxl8_point pos;
} pxl8_skyline_fit;
typedef struct pxl8_skyline_node {
i32 x, y, width;
} pxl8_skyline_node;
typedef struct pxl8_skyline {
pxl8_skyline_node* nodes;
u32 count;
u32 capacity;
} pxl8_skyline;
struct pxl8_atlas {
u32 height, width;
u8* pixels;
bool dirty;
u32 entry_capacity, entry_count;
pxl8_atlas_entry* entries;
u32 free_capacity, free_count;
u32* free_list;
pxl8_skyline skyline;
};
static pxl8_skyline_fit pxl8_skyline_find_position(
const pxl8_skyline* skyline,
u32 atlas_w,
u32 atlas_h,
u32 rect_w,
u32 rect_h
) {
pxl8_skyline_fit result = {.found = false};
i32 best_y = INT32_MAX;
i32 best_x = 0;
u32 best_idx = 0;
for (u32 i = 0; i < skyline->count; i++) {
i32 x = skyline->nodes[i].x;
i32 y = skyline->nodes[i].y;
if (x + (i32)rect_w > (i32)atlas_w) continue;
i32 max_y = y;
for (u32 j = i; j < skyline->count && skyline->nodes[j].x < x + (i32)rect_w; j++) {
if (skyline->nodes[j].y > max_y) {
max_y = skyline->nodes[j].y;
}
}
if (max_y + (i32)rect_h > (i32)atlas_h) continue;
if (max_y < best_y || (max_y == best_y && x < best_x)) {
best_y = max_y;
best_x = x;
best_idx = i;
}
}
if (best_y != INT32_MAX) {
result.found = true;
result.pos.x = best_x;
result.pos.y = best_y;
result.node_idx = best_idx;
}
return result;
}
static bool pxl8_skyline_add_rect(pxl8_skyline* skyline, pxl8_point pos, u32 w, u32 h) {
u32 node_idx = 0;
for (u32 i = 0; i < skyline->count; i++) {
if (skyline->nodes[i].x == pos.x) {
node_idx = i;
break;
}
}
u32 nodes_to_remove = 0;
for (u32 i = node_idx; i < skyline->count; i++) {
if (skyline->nodes[i].x < pos.x + (i32)w) {
nodes_to_remove++;
} else {
break;
}
}
if (skyline->count - nodes_to_remove + 1 > skyline->capacity) {
u32 new_capacity = (skyline->count - nodes_to_remove + 1) * 2;
pxl8_skyline_node* new_nodes = (pxl8_skyline_node*)realloc(
skyline->nodes,
new_capacity * sizeof(pxl8_skyline_node)
);
if (!new_nodes) return false;
skyline->nodes = new_nodes;
skyline->capacity = new_capacity;
}
if (nodes_to_remove > 0) {
memmove(
&skyline->nodes[node_idx + 1],
&skyline->nodes[node_idx + nodes_to_remove],
(skyline->count - node_idx - nodes_to_remove) * sizeof(pxl8_skyline_node)
);
}
skyline->nodes[node_idx] = (pxl8_skyline_node){pos.x, pos.y + (i32)h, (i32)w};
skyline->count = skyline->count - nodes_to_remove + 1;
return true;
}
static void pxl8_skyline_compact(pxl8_skyline* skyline) {
for (u32 i = 0; i < skyline->count - 1; ) {
if (skyline->nodes[i].y == skyline->nodes[i + 1].y) {
skyline->nodes[i].width += skyline->nodes[i + 1].width;
memmove(
&skyline->nodes[i + 1],
&skyline->nodes[i + 2],
(skyline->count - i - 2) * sizeof(pxl8_skyline_node)
);
skyline->count--;
} else {
i++;
}
}
}
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_pixel_mode pixel_mode) {
pxl8_atlas* atlas = (pxl8_atlas*)calloc(1, sizeof(pxl8_atlas));
if (!atlas) return NULL;
atlas->height = height;
atlas->width = width;
i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode);
atlas->pixels = (u8*)calloc(width * height, bytes_per_pixel);
if (!atlas->pixels) {
free(atlas);
return NULL;
}
atlas->entry_capacity = PXL8_DEFAULT_ATLAS_ENTRY_CAPACITY;
atlas->entries = (pxl8_atlas_entry*)calloc(atlas->entry_capacity, sizeof(pxl8_atlas_entry));
if (!atlas->entries) {
free(atlas->pixels);
free(atlas);
return NULL;
}
atlas->free_capacity = 16;
atlas->free_list = (u32*)calloc(atlas->free_capacity, sizeof(u32));
if (!atlas->free_list) {
free(atlas->entries);
free(atlas->pixels);
free(atlas);
return NULL;
}
atlas->skyline.capacity = 16;
atlas->skyline.nodes =
(pxl8_skyline_node*)calloc(atlas->skyline.capacity, sizeof(pxl8_skyline_node));
if (!atlas->skyline.nodes) {
free(atlas->free_list);
free(atlas->entries);
free(atlas->pixels);
free(atlas);
return NULL;
}
atlas->skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)width};
atlas->skyline.count = 1;
return atlas;
}
void pxl8_atlas_destroy(pxl8_atlas* atlas) {
if (!atlas) return;
free(atlas->entries);
free(atlas->free_list);
free(atlas->pixels);
free(atlas->skyline.nodes);
free(atlas);
}
void pxl8_atlas_clear(pxl8_atlas* atlas, u32 preserve_count) {
if (!atlas) return;
for (u32 i = preserve_count; i < atlas->entry_count; i++) {
atlas->entries[i].active = false;
}
atlas->entry_count = preserve_count;
atlas->free_count = 0;
atlas->skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)atlas->width};
atlas->skyline.count = 1;
atlas->dirty = true;
}
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode) {
if (!atlas || atlas->width >= 4096) return false;
i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode);
u32 new_size = atlas->width * 2;
u32 old_width = atlas->width;
u8* new_pixels = (u8*)calloc(new_size * new_size, bytes_per_pixel);
if (!new_pixels) return false;
pxl8_skyline new_skyline;
new_skyline.nodes = (pxl8_skyline_node*)calloc(16, sizeof(pxl8_skyline_node));
if (!new_skyline.nodes) {
free(new_pixels);
return false;
}
new_skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)new_size};
new_skyline.count = 1;
new_skyline.capacity = 16;
for (u32 i = 0; i < atlas->entry_count; i++) {
if (!atlas->entries[i].active) continue;
pxl8_skyline_fit fit = pxl8_skyline_find_position(
&new_skyline,
new_size,
new_size,
atlas->entries[i].w,
atlas->entries[i].h
);
if (!fit.found) {
free(new_skyline.nodes);
free(new_pixels);
return false;
}
for (u32 y = 0; y < (u32)atlas->entries[i].h; y++) {
for (u32 x = 0; x < (u32)atlas->entries[i].w; x++) {
u32 src_idx = (atlas->entries[i].y + y) * old_width + (atlas->entries[i].x + x);
u32 dst_idx = (fit.pos.y + y) * new_size + (fit.pos.x + x);
if (bytes_per_pixel == 2) {
((u16*)new_pixels)[dst_idx] = ((u16*)atlas->pixels)[src_idx];
} else {
new_pixels[dst_idx] = atlas->pixels[src_idx];
}
}
}
atlas->entries[i].x = fit.pos.x;
atlas->entries[i].y = fit.pos.y;
if (!pxl8_skyline_add_rect(&new_skyline, fit.pos, atlas->entries[i].w, atlas->entries[i].h)) {
free(new_skyline.nodes);
free(new_pixels);
return false;
}
pxl8_skyline_compact(&new_skyline);
}
free(atlas->pixels);
free(atlas->skyline.nodes);
atlas->pixels = new_pixels;
atlas->skyline = new_skyline;
atlas->width = new_size;
atlas->height = new_size;
atlas->dirty = true;
pxl8_debug("Atlas expanded to %ux%u", atlas->width, atlas->height);
return true;
}
u32 pxl8_atlas_add_texture(
pxl8_atlas* atlas,
const u8* pixels,
u32 w,
u32 h,
pxl8_pixel_mode pixel_mode
) {
if (!atlas || !pixels) return UINT32_MAX;
pxl8_skyline_fit fit =
pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h);
if (!fit.found) {
if (!pxl8_atlas_expand(atlas, pixel_mode)) {
return UINT32_MAX;
}
fit = pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h);
if (!fit.found) return UINT32_MAX;
}
u32 texture_id;
if (atlas->free_count > 0) {
texture_id = atlas->free_list[--atlas->free_count];
} else {
if (atlas->entry_count >= atlas->entry_capacity) {
u32 new_capacity = atlas->entry_capacity * 2;
pxl8_atlas_entry* new_entries = (pxl8_atlas_entry*)realloc(
atlas->entries,
new_capacity * sizeof(pxl8_atlas_entry)
);
if (!new_entries) return UINT32_MAX;
atlas->entries = new_entries;
atlas->entry_capacity = new_capacity;
}
texture_id = atlas->entry_count++;
}
pxl8_atlas_entry* entry = &atlas->entries[texture_id];
entry->active = true;
entry->texture_id = texture_id;
entry->x = fit.pos.x;
entry->y = fit.pos.y;
entry->w = w;
entry->h = h;
i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode);
for (u32 y = 0; y < h; y++) {
for (u32 x = 0; x < w; x++) {
u32 src_idx = y * w + x;
u32 dst_idx = (fit.pos.y + y) * atlas->width + (fit.pos.x + x);
if (bytes_per_pixel == 2) {
((u16*)atlas->pixels)[dst_idx] = ((const u16*)pixels)[src_idx];
} else {
atlas->pixels[dst_idx] = pixels[src_idx];
}
}
}
if (!pxl8_skyline_add_rect(&atlas->skyline, fit.pos, w, h)) {
entry->active = false;
return UINT32_MAX;
}
pxl8_skyline_compact(&atlas->skyline);
atlas->dirty = true;
return texture_id;
}
const pxl8_atlas_entry* pxl8_atlas_get_entry(const pxl8_atlas* atlas, u32 id) {
if (!atlas || id >= atlas->entry_count) return NULL;
return &atlas->entries[id];
}
u32 pxl8_atlas_get_entry_count(const pxl8_atlas* atlas) {
return atlas ? atlas->entry_count : 0;
}
u32 pxl8_atlas_get_height(const pxl8_atlas* atlas) {
return atlas ? atlas->height : 0;
}
const u8* pxl8_atlas_get_pixels(const pxl8_atlas* atlas) {
return atlas ? atlas->pixels : NULL;
}
u32 pxl8_atlas_get_width(const pxl8_atlas* atlas) {
return atlas ? atlas->width : 0;
}
bool pxl8_atlas_is_dirty(const pxl8_atlas* atlas) {
return atlas ? atlas->dirty : false;
}
void pxl8_atlas_mark_clean(pxl8_atlas* atlas) {
if (atlas) {
atlas->dirty = false;
}
}

View file

@ -1,35 +0,0 @@
#pragma once
#include "pxl8_types.h"
typedef struct pxl8_atlas pxl8_atlas;
typedef struct pxl8_atlas_entry {
bool active;
u32 texture_id;
i32 x, y, w, h;
} pxl8_atlas_entry;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_pixel_mode pixel_mode);
void pxl8_atlas_destroy(pxl8_atlas* atlas);
const pxl8_atlas_entry* pxl8_atlas_get_entry(const pxl8_atlas* atlas, u32 id);
u32 pxl8_atlas_get_entry_count(const pxl8_atlas* atlas);
u32 pxl8_atlas_get_height(const pxl8_atlas* atlas);
const u8* pxl8_atlas_get_pixels(const pxl8_atlas* atlas);
u32 pxl8_atlas_get_width(const pxl8_atlas* atlas);
bool pxl8_atlas_is_dirty(const pxl8_atlas* atlas);
void pxl8_atlas_mark_clean(pxl8_atlas* atlas);
u32 pxl8_atlas_add_texture(pxl8_atlas* atlas, const u8* pixels, u32 w, u32 h, pxl8_pixel_mode pixel_mode);
void pxl8_atlas_clear(pxl8_atlas* atlas, u32 preserve_count);
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode);
#ifdef __cplusplus
}
#endif

View file

@ -1,57 +0,0 @@
#include "pxl8_blit.h"
void pxl8_blit_hicolor(u16* fb, u32 fb_width, const u16* sprite, u32 atlas_width,
i32 x, i32 y, u32 w, u32 h) {
u16* dest_base = fb + y * fb_width + x;
const u16* src_base = sprite;
for (u32 row = 0; row < h; row++) {
u16* dest_row = dest_base + row * fb_width;
const u16* src_row = src_base + row * atlas_width;
u32 col = 0;
u32 count2 = w / 2;
for (u32 i = 0; i < count2; i++) {
u32 pixels = ((const u32*)src_row)[i];
if (pixels == 0) {
col += 2;
continue;
}
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) {
dest_row[col] = pxl8_blend_hicolor(src_row[col], dest_row[col]);
}
}
}
void pxl8_blit_indexed(u8* fb, u32 fb_width, const u8* sprite, u32 atlas_width,
i32 x, i32 y, u32 w, u32 h) {
u8* dest_base = fb + y * fb_width + x;
const u8* src_base = sprite;
for (u32 row = 0; row < h; row++) {
u8* dest_row = dest_base + row * fb_width;
const u8* src_row = src_base + row * atlas_width;
u32 col = 0;
u32 count4 = w / 4;
for (u32 i = 0; i < count4; i++) {
u32 pixels = ((const u32*)src_row)[i];
if (pixels == 0) {
col += 4;
continue;
}
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++) {
dest_row[col] = pxl8_blend_indexed(src_row[col], dest_row[col]);
}
}
}

View file

@ -1,32 +0,0 @@
#pragma once
#include "pxl8_types.h"
#ifdef __cplusplus
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,
i32 x, i32 y, u32 w, u32 h
);
void pxl8_blit_indexed(
u8* fb, u32 fb_width,
const u8* sprite, u32 atlas_width,
i32 x, i32 y, u32 w, u32 h
);
#ifdef __cplusplus
}
#endif

View file

@ -1,546 +0,0 @@
#include "pxl8_bsp.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pxl8_gfx.h"
#include "pxl8_io.h"
#include "pxl8_log.h"
#define BSP_VERSION 29
typedef enum {
CHUNK_ENTITIES = 0,
CHUNK_PLANES = 1,
CHUNK_TEXTURES = 2,
CHUNK_VERTICES = 3,
CHUNK_VISIBILITY = 4,
CHUNK_NODES = 5,
CHUNK_TEXINFO = 6,
CHUNK_FACES = 7,
CHUNK_LIGHTING = 8,
CHUNK_CLIPNODES = 9,
CHUNK_LEAFS = 10,
CHUNK_MARKSURFACES = 11,
CHUNK_EDGES = 12,
CHUNK_SURFEDGES = 13,
CHUNK_MODELS = 14,
CHUNK_COUNT = 15
} pxl8_bsp_chunk_type;
typedef struct {
u32 offset;
u32 size;
} pxl8_bsp_chunk;
typedef struct {
u32 version;
pxl8_bsp_chunk chunks[CHUNK_COUNT];
} pxl8_bsp_header;
static inline pxl8_vec3 read_vec3(pxl8_stream* stream) {
pxl8_vec3 v;
v.x = pxl8_read_f32(stream);
v.y = pxl8_read_f32(stream);
v.z = pxl8_read_f32(stream);
return v;
}
static bool validate_chunk(const pxl8_bsp_chunk* chunk, u32 element_size, size_t file_size) {
if (chunk->size == 0) return true;
if (chunk->offset >= file_size) return false;
if (chunk->offset + chunk->size > file_size) return false;
if (chunk->size % element_size != 0) return false;
return true;
}
static inline bool pxl8_bsp_get_edge_vertex(const pxl8_bsp* bsp, i32 surfedge_idx, u32* out_vert_idx) {
if (surfedge_idx >= (i32)bsp->num_surfedges) return false;
i32 edge_idx = bsp->surfedges[surfedge_idx];
u32 vertex_index;
if (edge_idx >= 0) {
if ((u32)edge_idx >= bsp->num_edges) return false;
vertex_index = 0;
} else {
edge_idx = -edge_idx;
if ((u32)edge_idx >= bsp->num_edges) return false;
vertex_index = 1;
}
*out_vert_idx = bsp->edges[edge_idx].vertex[vertex_index];
return *out_vert_idx < bsp->num_vertices;
}
static inline bool pxl8_bsp_get_edge_vertices(const pxl8_bsp* bsp, i32 surfedge_idx, u32* out_v0_idx, u32* out_v1_idx) {
if (surfedge_idx >= (i32)bsp->num_surfedges) return false;
i32 edge_idx = bsp->surfedges[surfedge_idx];
if (edge_idx >= 0) {
if ((u32)edge_idx >= bsp->num_edges) return false;
*out_v0_idx = bsp->edges[edge_idx].vertex[0];
*out_v1_idx = bsp->edges[edge_idx].vertex[1];
} else {
edge_idx = -edge_idx;
if ((u32)edge_idx >= bsp->num_edges) return false;
*out_v0_idx = bsp->edges[edge_idx].vertex[1];
*out_v1_idx = bsp->edges[edge_idx].vertex[0];
}
return *out_v0_idx < bsp->num_vertices && *out_v1_idx < bsp->num_vertices;
}
pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!path || !bsp) return PXL8_ERROR_INVALID_ARGUMENT;
memset(bsp, 0, sizeof(*bsp));
u8* file_data = NULL;
size_t file_size = 0;
pxl8_result result = pxl8_io_read_binary_file(path, &file_data, &file_size);
if (result != PXL8_OK) {
pxl8_error("Failed to load BSP file: %s", path);
return result;
}
if (file_size < sizeof(pxl8_bsp_header)) {
pxl8_error("BSP file too small: %s", path);
free(file_data);
return PXL8_ERROR_INVALID_FORMAT;
}
pxl8_stream stream = pxl8_stream_create(file_data, (u32)file_size);
pxl8_bsp_header header;
header.version = pxl8_read_u32(&stream);
if (header.version != BSP_VERSION) {
pxl8_error("Invalid BSP version: %u (expected %d)", header.version, BSP_VERSION);
free(file_data);
return PXL8_ERROR_INVALID_FORMAT;
}
for (i32 i = 0; i < CHUNK_COUNT; i++) {
header.chunks[i].offset = pxl8_read_u32(&stream);
header.chunks[i].size = pxl8_read_u32(&stream);
}
pxl8_bsp_chunk* chunk = &header.chunks[CHUNK_VERTICES];
if (!validate_chunk(chunk, 12, file_size)) goto error_cleanup;
bsp->num_vertices = chunk->size / 12;
if (bsp->num_vertices > 0) {
bsp->vertices = calloc(bsp->num_vertices, sizeof(pxl8_bsp_vertex));
if (!bsp->vertices) goto error_cleanup;
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_vertices; i++) {
bsp->vertices[i].position = read_vec3(&stream);
}
}
chunk = &header.chunks[CHUNK_EDGES];
if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup;
bsp->num_edges = chunk->size / 4;
if (bsp->num_edges > 0) {
bsp->edges = calloc(bsp->num_edges, sizeof(pxl8_bsp_edge));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_edges; i++) {
bsp->edges[i].vertex[0] = pxl8_read_u16(&stream);
bsp->edges[i].vertex[1] = pxl8_read_u16(&stream);
}
}
chunk = &header.chunks[CHUNK_SURFEDGES];
if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup;
bsp->num_surfedges = chunk->size / 4;
if (bsp->num_surfedges > 0) {
bsp->surfedges = calloc(bsp->num_surfedges, sizeof(i32));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_surfedges; i++) {
bsp->surfedges[i] = pxl8_read_i32(&stream);
}
}
chunk = &header.chunks[CHUNK_PLANES];
if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup;
bsp->num_planes = chunk->size / 20;
if (bsp->num_planes > 0) {
bsp->planes = calloc(bsp->num_planes, sizeof(pxl8_bsp_plane));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_planes; i++) {
bsp->planes[i].normal = read_vec3(&stream);
bsp->planes[i].dist = pxl8_read_f32(&stream);
bsp->planes[i].type = pxl8_read_i32(&stream);
}
}
chunk = &header.chunks[CHUNK_TEXINFO];
if (!validate_chunk(chunk, 40, file_size)) goto error_cleanup;
bsp->num_texinfo = chunk->size / 40;
if (bsp->num_texinfo > 0) {
bsp->texinfo = calloc(bsp->num_texinfo, sizeof(pxl8_bsp_texinfo));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_texinfo; i++) {
bsp->texinfo[i].u_axis = read_vec3(&stream);
bsp->texinfo[i].u_offset = pxl8_read_f32(&stream);
bsp->texinfo[i].v_axis = read_vec3(&stream);
bsp->texinfo[i].v_offset = pxl8_read_f32(&stream);
bsp->texinfo[i].miptex = pxl8_read_u32(&stream);
}
}
chunk = &header.chunks[CHUNK_FACES];
if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup;
bsp->num_faces = chunk->size / 20;
if (bsp->num_faces > 0) {
bsp->faces = calloc(bsp->num_faces, sizeof(pxl8_bsp_face));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_faces; i++) {
bsp->faces[i].plane_id = pxl8_read_u16(&stream);
bsp->faces[i].side = pxl8_read_u16(&stream);
bsp->faces[i].first_edge = pxl8_read_u32(&stream);
bsp->faces[i].num_edges = pxl8_read_u16(&stream);
bsp->faces[i].texinfo_id = pxl8_read_u16(&stream);
bsp->faces[i].styles[0] = pxl8_read_u8(&stream);
bsp->faces[i].styles[1] = pxl8_read_u8(&stream);
bsp->faces[i].styles[2] = pxl8_read_u8(&stream);
bsp->faces[i].styles[3] = pxl8_read_u8(&stream);
bsp->faces[i].lightmap_offset = pxl8_read_u32(&stream);
bsp->faces[i].aabb_min = (pxl8_vec3){1e30f, 1e30f, 1e30f};
bsp->faces[i].aabb_max = (pxl8_vec3){-1e30f, -1e30f, -1e30f};
}
}
chunk = &header.chunks[CHUNK_NODES];
if (!validate_chunk(chunk, 24, file_size)) goto error_cleanup;
bsp->num_nodes = chunk->size / 24;
if (bsp->num_nodes > 0) {
bsp->nodes = calloc(bsp->num_nodes, sizeof(pxl8_bsp_node));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_nodes; i++) {
bsp->nodes[i].plane_id = pxl8_read_u32(&stream);
bsp->nodes[i].children[0] = pxl8_read_i16(&stream);
bsp->nodes[i].children[1] = pxl8_read_i16(&stream);
for (u32 j = 0; j < 3; j++) bsp->nodes[i].mins[j] = pxl8_read_i16(&stream);
for (u32 j = 0; j < 3; j++) bsp->nodes[i].maxs[j] = pxl8_read_i16(&stream);
bsp->nodes[i].first_face = pxl8_read_u16(&stream);
bsp->nodes[i].num_faces = pxl8_read_u16(&stream);
}
}
chunk = &header.chunks[CHUNK_LEAFS];
if (!validate_chunk(chunk, 28, file_size)) goto error_cleanup;
bsp->num_leafs = chunk->size / 28;
if (bsp->num_leafs > 0) {
bsp->leafs = calloc(bsp->num_leafs, sizeof(pxl8_bsp_leaf));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_leafs; i++) {
bsp->leafs[i].contents = pxl8_read_i32(&stream);
bsp->leafs[i].visofs = pxl8_read_i32(&stream);
for (u32 j = 0; j < 3; j++) bsp->leafs[i].mins[j] = pxl8_read_i16(&stream);
for (u32 j = 0; j < 3; j++) bsp->leafs[i].maxs[j] = pxl8_read_i16(&stream);
bsp->leafs[i].first_marksurface = pxl8_read_u16(&stream);
bsp->leafs[i].num_marksurfaces = pxl8_read_u16(&stream);
for (u32 j = 0; j < 4; j++) bsp->leafs[i].ambient_level[j] = pxl8_read_u8(&stream);
}
}
chunk = &header.chunks[CHUNK_MARKSURFACES];
if (!validate_chunk(chunk, 2, file_size)) goto error_cleanup;
bsp->num_marksurfaces = chunk->size / 2;
if (bsp->num_marksurfaces > 0) {
bsp->marksurfaces = calloc(bsp->num_marksurfaces, sizeof(u16));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_marksurfaces; i++) {
bsp->marksurfaces[i] = pxl8_read_u16(&stream);
}
}
chunk = &header.chunks[CHUNK_MODELS];
if (!validate_chunk(chunk, 64, file_size)) goto error_cleanup;
bsp->num_models = chunk->size / 64;
if (bsp->num_models > 0) {
bsp->models = calloc(bsp->num_models, sizeof(pxl8_bsp_model));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_models; i++) {
for (u32 j = 0; j < 3; j++) bsp->models[i].mins[j] = pxl8_read_f32(&stream);
for (u32 j = 0; j < 3; j++) bsp->models[i].maxs[j] = pxl8_read_f32(&stream);
bsp->models[i].origin = read_vec3(&stream);
for (u32 j = 0; j < 4; j++) bsp->models[i].headnode[j] = pxl8_read_i32(&stream);
bsp->models[i].visleafs = pxl8_read_i32(&stream);
bsp->models[i].first_face = pxl8_read_i32(&stream);
bsp->models[i].num_faces = pxl8_read_i32(&stream);
}
}
chunk = &header.chunks[CHUNK_VISIBILITY];
if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup;
bsp->visdata_size = chunk->size;
if (bsp->visdata_size > 0) {
bsp->visdata = malloc(bsp->visdata_size);
memcpy(bsp->visdata, file_data + chunk->offset, bsp->visdata_size);
}
chunk = &header.chunks[CHUNK_LIGHTING];
if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup;
bsp->lightdata_size = chunk->size;
if (bsp->lightdata_size > 0) {
bsp->lightdata = malloc(bsp->lightdata_size);
memcpy(bsp->lightdata, file_data + chunk->offset, bsp->lightdata_size);
}
free(file_data);
for (u32 i = 0; i < bsp->num_faces; i++) {
pxl8_bsp_face* face = &bsp->faces[i];
f32 min_x = 1e30f, min_y = 1e30f, min_z = 1e30f;
f32 max_x = -1e30f, max_y = -1e30f, max_z = -1e30f;
for (u32 j = 0; j < face->num_edges; j++) {
i32 surfedge_idx = face->first_edge + j;
u32 vert_idx;
if (!pxl8_bsp_get_edge_vertex(bsp, surfedge_idx, &vert_idx)) continue;
pxl8_vec3 v = bsp->vertices[vert_idx].position;
if (v.x < min_x) min_x = v.x;
if (v.x > max_x) max_x = v.x;
if (v.y < min_y) min_y = v.y;
if (v.y > max_y) max_y = v.y;
if (v.z < min_z) min_z = v.z;
if (v.z > max_z) max_z = v.z;
}
face->aabb_min = (pxl8_vec3){min_x, min_y, min_z};
face->aabb_max = (pxl8_vec3){max_x, max_y, max_z};
}
pxl8_debug("Loaded BSP: %u verts, %u faces, %u nodes, %u leafs",
bsp->num_vertices, bsp->num_faces, bsp->num_nodes, bsp->num_leafs);
return PXL8_OK;
error_cleanup:
pxl8_error("BSP chunk validation failed: %s", path);
free(file_data);
pxl8_bsp_destroy(bsp);
return PXL8_ERROR_INVALID_FORMAT;
}
void pxl8_bsp_destroy(pxl8_bsp* bsp) {
if (!bsp) return;
free(bsp->edges);
free(bsp->faces);
free(bsp->leafs);
free(bsp->lightdata);
free(bsp->marksurfaces);
free(bsp->models);
free(bsp->nodes);
free(bsp->planes);
free(bsp->surfedges);
free(bsp->texinfo);
free(bsp->vertices);
free(bsp->visdata);
memset(bsp, 0, sizeof(*bsp));
}
i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos) {
if (!bsp || bsp->num_nodes == 0) return -1;
i32 node_id = 0;
while (node_id >= 0) {
const pxl8_bsp_node* node = &bsp->nodes[node_id];
const pxl8_bsp_plane* plane = &bsp->planes[node->plane_id];
f32 dist = pxl8_vec3_dot(pos, plane->normal) - plane->dist;
node_id = node->children[dist < 0 ? 1 : 0];
}
return -(node_id + 1);
}
bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to) {
if (!bsp || !bsp->visdata || leaf_from < 0 || leaf_to < 0) return true;
if ((u32)leaf_from >= bsp->num_leafs || (u32)leaf_to >= bsp->num_leafs) return true;
i32 visofs = bsp->leafs[leaf_from].visofs;
if (visofs < 0) return true;
i32 byte_idx = leaf_to >> 3;
i32 bit_idx = leaf_to & 7;
if ((u32)visofs + byte_idx >= bsp->visdata_size) return true;
return (bsp->visdata[visofs + byte_idx] & (1 << bit_idx)) != 0;
}
static inline bool face_in_frustum(const pxl8_bsp* bsp, u32 face_id, const pxl8_frustum* frustum) {
const pxl8_bsp_face* face = &bsp->faces[face_id];
return pxl8_frustum_test_aabb(frustum, face->aabb_min, face->aabb_max);
}
void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, u32 texture_id) {
if (!gfx || !bsp || face_id >= bsp->num_faces) return;
const pxl8_bsp_face* face = &bsp->faces[face_id];
if (face->num_edges < 3) return;
pxl8_vec3 verts[64];
u32 num_verts = 0;
u32 color = 15;
bool use_texture = (face->texinfo_id < bsp->num_texinfo);
for (u32 i = 0; i < face->num_edges && num_verts < 64; i++) {
i32 surfedge_idx = face->first_edge + i;
u32 vert_idx;
if (!pxl8_bsp_get_edge_vertex(bsp, surfedge_idx, &vert_idx)) continue;
verts[num_verts++] = bsp->vertices[vert_idx].position;
}
if (num_verts < 3) return;
if (use_texture && face->texinfo_id < bsp->num_texinfo) {
const pxl8_bsp_texinfo* texinfo = &bsp->texinfo[face->texinfo_id];
f32 tex_scale = 64.0f;
for (u32 tri_idx = 1; tri_idx < num_verts - 1; tri_idx++) {
pxl8_vec3 v0 = verts[0];
pxl8_vec3 v1 = verts[tri_idx];
pxl8_vec3 v2 = verts[tri_idx + 1];
f32 u0 = (pxl8_vec3_dot(v0, texinfo->u_axis) + texinfo->u_offset) / tex_scale;
f32 v0_uv = (pxl8_vec3_dot(v0, texinfo->v_axis) + texinfo->v_offset) / tex_scale;
f32 u1 = (pxl8_vec3_dot(v1, texinfo->u_axis) + texinfo->u_offset) / tex_scale;
f32 v1_uv = (pxl8_vec3_dot(v1, texinfo->v_axis) + texinfo->v_offset) / tex_scale;
f32 u2 = (pxl8_vec3_dot(v2, texinfo->u_axis) + texinfo->u_offset) / tex_scale;
f32 v2_uv = (pxl8_vec3_dot(v2, texinfo->v_axis) + texinfo->v_offset) / tex_scale;
pxl8_3d_draw_triangle_textured(gfx, v0, v1, v2, u0, v0_uv, u1, v1_uv, u2, v2_uv, texture_id);
}
} else{
for (u32 i = 1; i < num_verts - 1; i++) {
pxl8_3d_draw_triangle_raw(gfx, verts[0], verts[i], verts[i + 1], color);
}
}
}
void pxl8_bsp_render_textured(
pxl8_gfx* gfx,
const pxl8_bsp* bsp,
pxl8_vec3 camera_pos
) {
if (!gfx || !bsp || bsp->num_faces == 0) return;
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
if (!frustum) return;
i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
static u8* rendered_faces = NULL;
static u32 rendered_faces_capacity = 0;
if (rendered_faces_capacity < bsp->num_faces) {
u8* new_buffer = realloc(rendered_faces, bsp->num_faces);
if (!new_buffer) return;
rendered_faces = new_buffer;
rendered_faces_capacity = bsp->num_faces;
pxl8_debug("Allocated face tracking buffer: %u bytes", bsp->num_faces);
}
memset(rendered_faces, 0, bsp->num_faces);
for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) {
if (camera_leaf >= 0 && !pxl8_bsp_is_leaf_visible(bsp, camera_leaf, leaf_id)) continue;
const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id];
for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
u32 surf_idx = leaf->first_marksurface + i;
if (surf_idx >= bsp->num_marksurfaces) continue;
u32 face_id = bsp->marksurfaces[surf_idx];
if (face_id >= bsp->num_faces) continue;
if (rendered_faces[face_id]) continue;
rendered_faces[face_id] = 1;
if (!face_in_frustum(bsp, face_id, frustum)) continue;
const pxl8_bsp_face* face = &bsp->faces[face_id];
u32 texture_id = 0;
if (face->texinfo_id < bsp->num_texinfo) {
texture_id = bsp->texinfo[face->texinfo_id].miptex;
} else {
static bool warned = false;
if (!warned) {
pxl8_warn("Face %u has invalid texinfo_id %u (num_texinfo=%u)",
face_id, face->texinfo_id, bsp->num_texinfo);
warned = true;
}
}
pxl8_bsp_render_face(gfx, bsp, face_id, texture_id);
}
}
}
void pxl8_bsp_render_wireframe(
pxl8_gfx* gfx,
const pxl8_bsp* bsp,
pxl8_vec3 camera_pos,
u32 color
) {
if (!gfx || !bsp) return;
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
if (!frustum) return;
i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) {
if (camera_leaf >= 0 && !pxl8_bsp_is_leaf_visible(bsp, camera_leaf, leaf_id)) continue;
const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id];
for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
u32 surf_idx = leaf->first_marksurface + i;
if (surf_idx >= bsp->num_marksurfaces) continue;
u32 face_id = bsp->marksurfaces[surf_idx];
if (face_id >= bsp->num_faces) continue;
if (!face_in_frustum(bsp, face_id, frustum)) continue;
const pxl8_bsp_face* face = &bsp->faces[face_id];
for (u32 e = 0; e < face->num_edges; e++) {
i32 surfedge_idx = face->first_edge + e;
i32 next_surfedge_idx = face->first_edge + ((e + 1) % face->num_edges);
if (surfedge_idx >= (i32)bsp->num_surfedges ||
next_surfedge_idx >= (i32)bsp->num_surfedges) continue;
u32 v0_idx, v1_idx;
if (!pxl8_bsp_get_edge_vertices(bsp, surfedge_idx, &v0_idx, &v1_idx)) continue;
pxl8_vec3 p0 = bsp->vertices[v0_idx].position;
pxl8_vec3 p1 = bsp->vertices[v1_idx].position;
pxl8_3d_draw_line_3d(gfx, p0, p1, color);
}
}
}
}

View file

@ -1,140 +0,0 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_math.h"
#include "pxl8_types.h"
typedef struct pxl8_bsp_edge {
u16 vertex[2];
} pxl8_bsp_edge;
typedef struct pxl8_bsp_face {
u32 first_edge;
u32 lightmap_offset;
u16 num_edges;
u16 plane_id;
u16 side;
u8 styles[4];
u16 texinfo_id;
pxl8_vec3 aabb_min;
pxl8_vec3 aabb_max;
} pxl8_bsp_face;
typedef struct pxl8_bsp_leaf {
u8 ambient_level[4];
i32 contents;
u16 first_marksurface;
i16 maxs[3];
i16 mins[3];
u16 num_marksurfaces;
i32 visofs;
} pxl8_bsp_leaf;
typedef struct pxl8_bsp_model {
i32 first_face;
i32 headnode[4];
f32 maxs[3];
f32 mins[3];
i32 num_faces;
pxl8_vec3 origin;
i32 visleafs;
} pxl8_bsp_model;
typedef struct pxl8_bsp_node {
i32 children[2];
u16 first_face;
i16 maxs[3];
i16 mins[3];
u16 num_faces;
u32 plane_id;
} pxl8_bsp_node;
typedef struct pxl8_bsp_plane {
f32 dist;
pxl8_vec3 normal;
i32 type;
} pxl8_bsp_plane;
typedef struct pxl8_bsp_texinfo {
u32 miptex;
char name[16];
f32 u_offset;
pxl8_vec3 u_axis;
f32 v_offset;
pxl8_vec3 v_axis;
} pxl8_bsp_texinfo;
typedef struct pxl8_bsp_vertex {
pxl8_vec3 position;
} pxl8_bsp_vertex;
typedef struct pxl8_bsp {
pxl8_bsp_edge* edges;
pxl8_bsp_face* faces;
pxl8_bsp_leaf* leafs;
u8* lightdata;
u16* marksurfaces;
pxl8_bsp_model* models;
pxl8_bsp_node* nodes;
pxl8_bsp_plane* planes;
i32* surfedges;
pxl8_bsp_texinfo* texinfo;
pxl8_bsp_vertex* vertices;
u8* visdata;
u32 lightdata_size;
u32 num_edges;
u32 num_faces;
u32 num_leafs;
u32 num_marksurfaces;
u32 num_models;
u32 num_nodes;
u32 num_planes;
u32 num_surfedges;
u32 num_texinfo;
u32 num_vertices;
u32 visdata_size;
} pxl8_bsp;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp);
void pxl8_bsp_destroy(pxl8_bsp* bsp);
i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos);
bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to);
void pxl8_bsp_render_face(
pxl8_gfx* gfx,
const pxl8_bsp* bsp,
u32 face_id,
u32 texture_id
);
void pxl8_bsp_render_textured(
pxl8_gfx* gfx,
const pxl8_bsp* bsp,
pxl8_vec3 camera_pos
);
void pxl8_bsp_render_wireframe(
pxl8_gfx* gfx,
const pxl8_bsp* bsp,
pxl8_vec3 camera_pos,
u32 color
);
#ifdef __cplusplus
}
#endif

View file

@ -1,634 +0,0 @@
#include "pxl8_cart.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "pxl8_log.h"
#define PXL8_CART_MAGIC 0x43585850
#define PXL8_CART_VERSION 1
#define PXL8_CART_TRAILER_MAGIC 0x544E4250
typedef struct {
u32 magic;
u16 version;
u16 flags;
u32 file_count;
u32 toc_size;
} pxl8_cart_header;
typedef struct {
u32 offset;
u32 size;
u16 path_len;
} pxl8_cart_entry;
typedef struct {
u32 magic;
u32 cart_offset;
u32 cart_size;
} pxl8_cart_trailer;
typedef struct {
char* path;
u32 offset;
u32 size;
} pxl8_cart_file;
struct pxl8_cart {
u8* data;
u32 data_size;
pxl8_cart_file* files;
u32 file_count;
char* base_path;
char* title;
pxl8_resolution resolution;
pxl8_size window_size;
pxl8_pixel_mode pixel_mode;
bool is_folder;
bool is_mounted;
};
static pxl8_cart* pxl8_current_cart = NULL;
static char* pxl8_original_cwd = NULL;
static bool is_directory(const char* path) {
struct stat st;
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
}
static bool is_pxc_file(const char* path) {
size_t len = strlen(path);
return len > 4 && strcmp(path + len - 4, ".pxc") == 0;
}
static bool has_main_script(const char* base_path) {
char path[512];
snprintf(path, sizeof(path), "%s/main.fnl", base_path);
if (access(path, F_OK) == 0) return true;
snprintf(path, sizeof(path), "%s/main.lua", base_path);
return access(path, F_OK) == 0;
}
static void collect_files_recursive(const char* dir_path, const char* prefix,
char*** paths, u32* count, u32* capacity) {
DIR* dir = opendir(dir_path);
if (!dir) return;
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
char full_path[1024];
char rel_path[1024];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
snprintf(rel_path, sizeof(rel_path), "%s%s", prefix, entry->d_name);
struct stat st;
if (stat(full_path, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
char new_prefix[1025];
snprintf(new_prefix, sizeof(new_prefix), "%s/", rel_path);
collect_files_recursive(full_path, new_prefix, paths, count, capacity);
} else {
if (*count >= *capacity) {
*capacity = (*capacity == 0) ? 64 : (*capacity * 2);
*paths = realloc(*paths, *capacity * sizeof(char*));
}
(*paths)[(*count)++] = strdup(rel_path);
}
}
}
closedir(dir);
}
static pxl8_cart_file* find_file(const pxl8_cart* cart, const char* path) {
if (!cart || !cart->files) return NULL;
for (u32 i = 0; i < cart->file_count; i++) {
if (strcmp(cart->files[i].path, path) == 0) {
return &cart->files[i];
}
}
return NULL;
}
static pxl8_result load_packed_cart(pxl8_cart* cart, const u8* data, u32 size) {
if (size < sizeof(pxl8_cart_header)) return PXL8_ERROR_INVALID_FORMAT;
const pxl8_cart_header* header = (const pxl8_cart_header*)data;
if (header->magic != PXL8_CART_MAGIC) return PXL8_ERROR_INVALID_FORMAT;
if (header->version > PXL8_CART_VERSION) return PXL8_ERROR_INVALID_FORMAT;
cart->data = malloc(size);
if (!cart->data) return PXL8_ERROR_OUT_OF_MEMORY;
memcpy(cart->data, data, size);
cart->data_size = size;
cart->file_count = header->file_count;
cart->files = calloc(cart->file_count, sizeof(pxl8_cart_file));
if (!cart->files) return PXL8_ERROR_OUT_OF_MEMORY;
const u8* toc = cart->data + sizeof(pxl8_cart_header);
for (u32 i = 0; i < cart->file_count; i++) {
const pxl8_cart_entry* entry = (const pxl8_cart_entry*)toc;
toc += sizeof(pxl8_cart_entry);
cart->files[i].path = malloc(entry->path_len + 1);
memcpy(cart->files[i].path, toc, entry->path_len);
cart->files[i].path[entry->path_len] = '\0';
toc += entry->path_len;
cart->files[i].offset = entry->offset;
cart->files[i].size = entry->size;
}
cart->is_folder = false;
return PXL8_OK;
}
pxl8_cart* pxl8_cart_create(void) {
pxl8_cart* cart = calloc(1, sizeof(pxl8_cart));
if (cart) {
cart->resolution = PXL8_RESOLUTION_640x360;
cart->window_size = (pxl8_size){1280, 720};
cart->pixel_mode = PXL8_PIXEL_INDEXED;
}
return cart;
}
pxl8_cart* pxl8_get_cart(void) {
return pxl8_current_cart;
}
void pxl8_cart_destroy(pxl8_cart* cart) {
if (!cart) return;
pxl8_cart_unload(cart);
free(cart);
}
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) {
if (!cart || !path) return PXL8_ERROR_NULL_POINTER;
pxl8_cart_unload(cart);
if (is_directory(path)) {
cart->base_path = realpath(path, NULL);
if (!cart->base_path) {
pxl8_error("Failed to resolve cart path: %s", path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
cart->is_folder = true;
if (!has_main_script(cart->base_path)) {
pxl8_error("No main.fnl or main.lua found in cart: %s", path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
pxl8_info("Loaded cart");
return PXL8_OK;
}
if (is_pxc_file(path)) {
FILE* file = fopen(path, "rb");
if (!file) {
pxl8_error("Failed to open cart: %s", path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
fseek(file, 0, SEEK_END);
u32 size = (u32)ftell(file);
fseek(file, 0, SEEK_SET);
u8* data = malloc(size);
if (!data || fread(data, 1, size, file) != size) {
free(data);
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fclose(file);
pxl8_result result = load_packed_cart(cart, data, size);
free(data);
if (result == PXL8_OK) {
pxl8_info("Loaded cart");
}
return result;
}
pxl8_error("Unknown cart format: %s", path);
return PXL8_ERROR_INVALID_FORMAT;
}
pxl8_result pxl8_cart_load_embedded(pxl8_cart* cart, const char* exe_path) {
if (!cart || !exe_path) return PXL8_ERROR_NULL_POINTER;
FILE* file = fopen(exe_path, "rb");
if (!file) return PXL8_ERROR_FILE_NOT_FOUND;
fseek(file, -(i32)sizeof(pxl8_cart_trailer), SEEK_END);
pxl8_cart_trailer trailer;
if (fread(&trailer, sizeof(trailer), 1, file) != 1) {
fclose(file);
return PXL8_ERROR_INVALID_FORMAT;
}
if (trailer.magic != PXL8_CART_TRAILER_MAGIC) {
fclose(file);
return PXL8_ERROR_INVALID_FORMAT;
}
fseek(file, trailer.cart_offset, SEEK_SET);
u8* data = malloc(trailer.cart_size);
if (!data || fread(data, 1, trailer.cart_size, file) != trailer.cart_size) {
free(data);
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fclose(file);
pxl8_result result = load_packed_cart(cart, data, trailer.cart_size);
free(data);
if (result == PXL8_OK) {
pxl8_info("Loaded embedded cart");
}
return result;
}
void pxl8_cart_unload(pxl8_cart* cart) {
if (!cart) return;
if (cart->title) {
pxl8_info("Unloaded cart: %s", cart->title);
pxl8_cart_unmount(cart);
free(cart->title);
cart->title = NULL;
} else if (cart->base_path || cart->data) {
pxl8_info("Unloaded cart");
pxl8_cart_unmount(cart);
}
if (cart->files) {
for (u32 i = 0; i < cart->file_count; i++) {
free(cart->files[i].path);
}
free(cart->files);
cart->files = NULL;
}
cart->file_count = 0;
free(cart->data);
cart->data = NULL;
cart->data_size = 0;
free(cart->base_path);
cart->base_path = NULL;
cart->is_folder = false;
}
pxl8_result pxl8_cart_mount(pxl8_cart* cart) {
if (!cart) return PXL8_ERROR_NULL_POINTER;
if (cart->is_mounted) return PXL8_OK;
if (pxl8_current_cart) {
pxl8_cart_unmount(pxl8_current_cart);
}
if (cart->is_folder) {
pxl8_original_cwd = getcwd(NULL, 0);
if (chdir(cart->base_path) != 0) {
pxl8_error("Failed to change to cart directory: %s", cart->base_path);
free(pxl8_original_cwd);
pxl8_original_cwd = NULL;
return PXL8_ERROR_FILE_NOT_FOUND;
}
}
cart->is_mounted = true;
pxl8_current_cart = cart;
if (cart->title) {
pxl8_info("Mounted cart: %s", cart->title);
} else {
pxl8_info("Mounted cart");
}
return PXL8_OK;
}
void pxl8_cart_unmount(pxl8_cart* cart) {
if (!cart || !cart->is_mounted) return;
if (pxl8_original_cwd) {
chdir(pxl8_original_cwd);
free(pxl8_original_cwd);
pxl8_original_cwd = NULL;
}
cart->is_mounted = false;
if (pxl8_current_cart == cart) {
pxl8_current_cart = NULL;
}
}
const char* pxl8_cart_get_base_path(const pxl8_cart* cart) {
return cart ? cart->base_path : NULL;
}
const char* pxl8_cart_get_title(const pxl8_cart* cart) {
return cart ? cart->title : NULL;
}
void pxl8_cart_set_title(pxl8_cart* cart, const char* title) {
if (!cart || !title) return;
free(cart->title);
cart->title = strdup(title);
}
pxl8_resolution pxl8_cart_get_resolution(const pxl8_cart* cart) {
return cart ? cart->resolution : PXL8_RESOLUTION_640x360;
}
void pxl8_cart_set_resolution(pxl8_cart* cart, pxl8_resolution resolution) {
if (cart) cart->resolution = resolution;
}
pxl8_size pxl8_cart_get_window_size(const pxl8_cart* cart) {
return cart ? cart->window_size : (pxl8_size){1280, 720};
}
void pxl8_cart_set_window_size(pxl8_cart* cart, pxl8_size size) {
if (cart) cart->window_size = size;
}
pxl8_pixel_mode pxl8_cart_get_pixel_mode(const pxl8_cart* cart) {
return cart ? cart->pixel_mode : PXL8_PIXEL_INDEXED;
}
void pxl8_cart_set_pixel_mode(pxl8_cart* cart, pxl8_pixel_mode mode) {
if (cart) cart->pixel_mode = mode;
}
bool pxl8_cart_is_packed(const pxl8_cart* cart) {
return cart && !cart->is_folder;
}
bool pxl8_cart_has_embedded(const char* exe_path) {
FILE* file = fopen(exe_path, "rb");
if (!file) return false;
fseek(file, -(i32)sizeof(pxl8_cart_trailer), SEEK_END);
pxl8_cart_trailer trailer;
bool has = (fread(&trailer, sizeof(trailer), 1, file) == 1 &&
trailer.magic == PXL8_CART_TRAILER_MAGIC);
fclose(file);
return has;
}
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path) {
if (!cart || !path) return false;
if (cart->is_folder) {
char full_path[512];
if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) return false;
return access(full_path, F_OK) == 0;
}
return find_file(cart, path) != NULL;
}
bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size) {
if (!cart || !relative_path || !out_path || out_size == 0) return false;
if (cart->is_folder && cart->base_path) {
i32 written = snprintf(out_path, out_size, "%s/%s", cart->base_path, relative_path);
return written >= 0 && (size_t)written < out_size;
}
i32 written = snprintf(out_path, out_size, "%s", relative_path);
return written >= 0 && (size_t)written < out_size;
}
pxl8_result pxl8_cart_read_file(const pxl8_cart* cart, const char* path, u8** data_out, u32* size_out) {
if (!cart || !path || !data_out || !size_out) return PXL8_ERROR_NULL_POINTER;
if (cart->is_folder) {
char full_path[512];
if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) {
return PXL8_ERROR_FILE_NOT_FOUND;
}
FILE* file = fopen(full_path, "rb");
if (!file) return PXL8_ERROR_FILE_NOT_FOUND;
fseek(file, 0, SEEK_END);
*size_out = (u32)ftell(file);
fseek(file, 0, SEEK_SET);
*data_out = malloc(*size_out);
if (!*data_out || fread(*data_out, 1, *size_out, file) != *size_out) {
free(*data_out);
*data_out = NULL;
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fclose(file);
return PXL8_OK;
}
pxl8_cart_file* cf = find_file(cart, path);
if (!cf) return PXL8_ERROR_FILE_NOT_FOUND;
*size_out = cf->size;
*data_out = malloc(cf->size);
if (!*data_out) return PXL8_ERROR_OUT_OF_MEMORY;
memcpy(*data_out, cart->data + cf->offset, cf->size);
return PXL8_OK;
}
void pxl8_cart_free_file(u8* data) {
free(data);
}
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) {
if (!folder_path || !output_path) return PXL8_ERROR_NULL_POINTER;
if (!is_directory(folder_path)) {
pxl8_error("Cart folder not found: %s", folder_path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
if (!has_main_script(folder_path)) {
pxl8_error("No main.fnl or main.lua found in cart: %s", folder_path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
char** paths = NULL;
u32 count = 0, capacity = 0;
collect_files_recursive(folder_path, "", &paths, &count, &capacity);
if (count == 0) {
pxl8_error("No files found in cart folder");
return PXL8_ERROR_FILE_NOT_FOUND;
}
u32 toc_size = 0;
for (u32 i = 0; i < count; i++) {
toc_size += sizeof(pxl8_cart_entry) + strlen(paths[i]);
}
u32 data_offset = sizeof(pxl8_cart_header) + toc_size;
u32 total_size = data_offset;
u32* file_sizes = malloc(count * sizeof(u32));
for (u32 i = 0; i < count; i++) {
char full_path[1024];
snprintf(full_path, sizeof(full_path), "%s/%s", folder_path, paths[i]);
struct stat st;
if (stat(full_path, &st) == 0) {
file_sizes[i] = (u32)st.st_size;
total_size += file_sizes[i];
} else {
file_sizes[i] = 0;
}
}
u8* buffer = calloc(1, total_size);
if (!buffer) {
free(file_sizes);
for (u32 i = 0; i < count; i++) free(paths[i]);
free(paths);
return PXL8_ERROR_OUT_OF_MEMORY;
}
pxl8_cart_header* header = (pxl8_cart_header*)buffer;
header->magic = PXL8_CART_MAGIC;
header->version = PXL8_CART_VERSION;
header->flags = 0;
header->file_count = count;
header->toc_size = toc_size;
u8* toc = buffer + sizeof(pxl8_cart_header);
u32 file_offset = data_offset;
pxl8_info("Packing cart: %s -> %s", folder_path, output_path);
for (u32 i = 0; i < count; i++) {
pxl8_cart_entry* entry = (pxl8_cart_entry*)toc;
entry->offset = file_offset;
entry->size = file_sizes[i];
entry->path_len = (u16)strlen(paths[i]);
toc += sizeof(pxl8_cart_entry);
memcpy(toc, paths[i], entry->path_len);
toc += entry->path_len;
char full_path[1024];
snprintf(full_path, sizeof(full_path), "%s/%s", folder_path, paths[i]);
FILE* file = fopen(full_path, "rb");
if (file) {
fread(buffer + file_offset, 1, file_sizes[i], file);
fclose(file);
pxl8_info(" %s (%u bytes)", paths[i], file_sizes[i]);
}
file_offset += file_sizes[i];
free(paths[i]);
}
free(paths);
free(file_sizes);
FILE* out = fopen(output_path, "wb");
if (!out) {
free(buffer);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fwrite(buffer, 1, total_size, out);
fclose(out);
free(buffer);
pxl8_info("Cart packed: %u files, %u bytes", count, total_size);
return PXL8_OK;
}
pxl8_result pxl8_cart_bundle(const char* input_path, const char* output_path, const char* exe_path) {
if (!input_path || !output_path || !exe_path) return PXL8_ERROR_NULL_POINTER;
u8* cart_data = NULL;
u32 cart_size = 0;
bool free_cart = false;
if (is_directory(input_path)) {
char temp_pxc[256];
snprintf(temp_pxc, sizeof(temp_pxc), "/tmp/pxl8_bundle_%d.pxc", getpid());
pxl8_result result = pxl8_cart_pack(input_path, temp_pxc);
if (result != PXL8_OK) return result;
FILE* f = fopen(temp_pxc, "rb");
if (!f) return PXL8_ERROR_SYSTEM_FAILURE;
fseek(f, 0, SEEK_END);
cart_size = (u32)ftell(f);
fseek(f, 0, SEEK_SET);
cart_data = malloc(cart_size);
fread(cart_data, 1, cart_size, f);
fclose(f);
unlink(temp_pxc);
free_cart = true;
} else if (is_pxc_file(input_path)) {
FILE* f = fopen(input_path, "rb");
if (!f) return PXL8_ERROR_FILE_NOT_FOUND;
fseek(f, 0, SEEK_END);
cart_size = (u32)ftell(f);
fseek(f, 0, SEEK_SET);
cart_data = malloc(cart_size);
fread(cart_data, 1, cart_size, f);
fclose(f);
free_cart = true;
} else {
return PXL8_ERROR_INVALID_FORMAT;
}
FILE* exe = fopen(exe_path, "rb");
if (!exe) {
if (free_cart) free(cart_data);
return PXL8_ERROR_FILE_NOT_FOUND;
}
fseek(exe, 0, SEEK_END);
u32 exe_size = (u32)ftell(exe);
fseek(exe, 0, SEEK_SET);
u8* exe_data = malloc(exe_size);
fread(exe_data, 1, exe_size, exe);
fclose(exe);
FILE* out = fopen(output_path, "wb");
if (!out) {
free(exe_data);
if (free_cart) free(cart_data);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fwrite(exe_data, 1, exe_size, out);
free(exe_data);
u32 cart_offset = exe_size;
fwrite(cart_data, 1, cart_size, out);
if (free_cart) free(cart_data);
pxl8_cart_trailer trailer = {
.magic = PXL8_CART_TRAILER_MAGIC,
.cart_offset = cart_offset,
.cart_size = cart_size
};
fwrite(&trailer, sizeof(trailer), 1, out);
fclose(out);
chmod(output_path, 0755);
pxl8_info("Bundle created: %s", output_path);
return PXL8_OK;
}

View file

@ -1,43 +0,0 @@
#pragma once
#include "pxl8_types.h"
typedef struct pxl8_cart pxl8_cart;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_cart* pxl8_cart_create(void);
void pxl8_cart_destroy(pxl8_cart* cart);
pxl8_result pxl8_cart_bundle(const char* input_path, const char* output_path, const char* exe_path);
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path);
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path);
pxl8_result pxl8_cart_load_embedded(pxl8_cart* cart, const char* exe_path);
void pxl8_cart_unload(pxl8_cart* cart);
pxl8_result pxl8_cart_mount(pxl8_cart* cart);
void pxl8_cart_unmount(pxl8_cart* cart);
pxl8_cart* pxl8_get_cart(void);
const char* pxl8_cart_get_base_path(const pxl8_cart* cart);
const char* pxl8_cart_get_title(const pxl8_cart* cart);
pxl8_resolution pxl8_cart_get_resolution(const pxl8_cart* cart);
pxl8_size pxl8_cart_get_window_size(const pxl8_cart* cart);
pxl8_pixel_mode pxl8_cart_get_pixel_mode(const pxl8_cart* cart);
void pxl8_cart_set_title(pxl8_cart* cart, const char* title);
void pxl8_cart_set_resolution(pxl8_cart* cart, pxl8_resolution resolution);
void pxl8_cart_set_window_size(pxl8_cart* cart, pxl8_size size);
void pxl8_cart_set_pixel_mode(pxl8_cart* cart, pxl8_pixel_mode mode);
bool pxl8_cart_is_packed(const pxl8_cart* cart);
bool pxl8_cart_has_embedded(const char* exe_path);
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path);
bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size);
pxl8_result pxl8_cart_read_file(const pxl8_cart* cart, const char* path, u8** data_out, u32* size_out);
void pxl8_cart_free_file(u8* data);
#ifdef __cplusplus
}
#endif

View file

@ -1,42 +0,0 @@
#pragma once
#include "pxl8_types.h"
static inline i32 pxl8_bytes_per_pixel(pxl8_pixel_mode mode) {
return (mode == PXL8_PIXEL_HICOLOR) ? 2 : 1;
}
static inline u16 pxl8_rgb565_pack(u8 r, u8 g, u8 b) {
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}
static inline void pxl8_rgb565_unpack(u16 color, u8* r, u8* g, u8* b) {
*r = (color >> 11) << 3;
*g = ((color >> 5) & 0x3F) << 2;
*b = (color & 0x1F) << 3;
}
static inline u16 pxl8_rgba32_to_rgb565(u32 rgba) {
return pxl8_rgb565_pack(rgba & 0xFF, (rgba >> 8) & 0xFF, (rgba >> 16) & 0xFF);
}
static inline u32 pxl8_rgb565_to_rgba32(u16 color) {
u8 r, g, b;
pxl8_rgb565_unpack(color, &r, &g, &b);
return r | (g << 8) | (b << 16) | 0xFF000000;
}
static inline void pxl8_rgba32_unpack(u32 color, u8* r, u8* g, u8* b, u8* a) {
*r = color & 0xFF;
*g = (color >> 8) & 0xFF;
*b = (color >> 16) & 0xFF;
*a = (color >> 24) & 0xFF;
}
static inline u32 pxl8_rgba32_pack(u8 r, u8 g, u8 b, u8 a) {
return r | (g << 8) | (b << 16) | (a << 24);
}
static inline u8 pxl8_color_lerp_channel(u8 c1, u8 c2, f32 t) {
return c1 + (i32)((c2 - c1) * t);
}

View file

@ -1,93 +0,0 @@
#pragma once
#include "pxl8_types.h"
#include <string.h>
static const char embed_fennel[] = {
#embed "lib/fennel/fennel.lua"
, 0 };
static const char embed_pxl8[] = {
#embed "src/lua/pxl8.lua"
, 0 };
static const char embed_pxl8_anim[] = {
#embed "src/lua/pxl8/anim.lua"
, 0 };
static const char embed_pxl8_core[] = {
#embed "src/lua/pxl8/core.lua"
, 0 };
static const char embed_pxl8_gfx2d[] = {
#embed "src/lua/pxl8/gfx2d.lua"
, 0 };
static const char embed_pxl8_gfx3d[] = {
#embed "src/lua/pxl8/gfx3d.lua"
, 0 };
static const char embed_pxl8_gui[] = {
#embed "src/lua/pxl8/gui.lua"
, 0 };
static const char embed_pxl8_input[] = {
#embed "src/lua/pxl8/input.lua"
, 0 };
static const char embed_pxl8_math[] = {
#embed "src/lua/pxl8/math.lua"
, 0 };
static const char embed_pxl8_particles[] = {
#embed "src/lua/pxl8/particles.lua"
, 0 };
static const char embed_pxl8_sfx[] = {
#embed "src/lua/pxl8/sfx.lua"
, 0 };
static const char embed_pxl8_tilemap[] = {
#embed "src/lua/pxl8/tilemap.lua"
, 0 };
static const char embed_pxl8_transition[] = {
#embed "src/lua/pxl8/transition.lua"
, 0 };
static const char embed_pxl8_vfx[] = {
#embed "src/lua/pxl8/vfx.lua"
, 0 };
static const char embed_pxl8_world[] = {
#embed "src/lua/pxl8/world.lua"
, 0 };
#define PXL8_EMBED_ENTRY(var, name) {name, var, sizeof(var) - 1}
typedef struct { const char* name; const char* data; u32 size; } pxl8_embed;
static const pxl8_embed pxl8_embeds[] = {
PXL8_EMBED_ENTRY(embed_fennel, "fennel"),
PXL8_EMBED_ENTRY(embed_pxl8, "pxl8"),
PXL8_EMBED_ENTRY(embed_pxl8_anim, "pxl8.anim"),
PXL8_EMBED_ENTRY(embed_pxl8_core, "pxl8.core"),
PXL8_EMBED_ENTRY(embed_pxl8_gfx2d, "pxl8.gfx2d"),
PXL8_EMBED_ENTRY(embed_pxl8_gfx3d, "pxl8.gfx3d"),
PXL8_EMBED_ENTRY(embed_pxl8_gui, "pxl8.gui"),
PXL8_EMBED_ENTRY(embed_pxl8_input, "pxl8.input"),
PXL8_EMBED_ENTRY(embed_pxl8_math, "pxl8.math"),
PXL8_EMBED_ENTRY(embed_pxl8_particles, "pxl8.particles"),
PXL8_EMBED_ENTRY(embed_pxl8_sfx, "pxl8.sfx"),
PXL8_EMBED_ENTRY(embed_pxl8_tilemap, "pxl8.tilemap"),
PXL8_EMBED_ENTRY(embed_pxl8_transition, "pxl8.transition"),
PXL8_EMBED_ENTRY(embed_pxl8_vfx, "pxl8.vfx"),
PXL8_EMBED_ENTRY(embed_pxl8_world, "pxl8.world"),
{0}
};
static inline const pxl8_embed* pxl8_embed_find(const char* name) {
for (const pxl8_embed* e = pxl8_embeds; e->name; e++)
if (strcmp(e->name, name) == 0) return e;
return NULL;
}

View file

@ -1,61 +0,0 @@
#include "pxl8_font.h"
#include <stdlib.h>
#include <string.h>
pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* atlas_width, i32* atlas_height) {
if (!font || !atlas_data || !atlas_width || !atlas_height) {
return PXL8_ERROR_NULL_POINTER;
}
i32 glyphs_per_row = 16;
i32 rows_needed = (font->glyph_count + glyphs_per_row - 1) / glyphs_per_row;
*atlas_width = glyphs_per_row * font->default_width;
*atlas_height = rows_needed * font->default_height;
i32 atlas_size = (*atlas_width) * (*atlas_height);
*atlas_data = (u8*)malloc(atlas_size);
if (!*atlas_data) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
memset(*atlas_data, 0, atlas_size);
for (u32 i = 0; i < font->glyph_count; i++) {
const pxl8_glyph* glyph = &font->glyphs[i];
i32 atlas_x = (i % glyphs_per_row) * font->default_width;
i32 atlas_y = (i / glyphs_per_row) * font->default_height;
for (i32 y = 0; y < glyph->height && y < font->default_height; y++) {
for (i32 x = 0; x < glyph->width && x < font->default_width; x++) {
i32 atlas_idx = (atlas_y + y) * (*atlas_width) + (atlas_x + x);
if (glyph->format == PXL8_FONT_FORMAT_INDEXED) {
u8 pixel_byte = glyph->data.indexed[y];
u8 pixel_bit = (pixel_byte >> x) & 1;
(*atlas_data)[atlas_idx] = pixel_bit ? 255 : 0;
} else {
i32 glyph_idx = y * 8 + x;
u32 rgba_pixel = glyph->data.rgba[glyph_idx];
(*atlas_data)[atlas_idx] = (rgba_pixel >> 24) & 0xFF;
}
}
}
}
return PXL8_OK;
}
const pxl8_glyph* pxl8_font_find_glyph(const pxl8_font* font, u32 codepoint) {
if (!font || !font->glyphs) return NULL;
for (u32 i = 0; i < font->glyph_count; i++) {
if (font->glyphs[i].codepoint == codepoint) {
return &font->glyphs[i];
}
}
return NULL;
}

View file

@ -1,130 +0,0 @@
#pragma once
#include "pxl8_types.h"
#define PXL8_FONT_FORMAT_INDEXED 0
#define PXL8_FONT_FORMAT_RGBA 1
typedef struct pxl8_glyph {
u32 codepoint;
u8 width;
u8 height;
u8 format;
union {
u8 indexed[64];
u32 rgba[64];
} data;
} pxl8_glyph;
typedef struct pxl8_font {
const pxl8_glyph* glyphs;
u32 glyph_count;
u8 default_width;
u8 default_height;
} pxl8_font;
static const pxl8_glyph pxl8_ascii_glyphs[] = {
{ 32, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } },
{ 33, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00 } } },
{ 34, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } },
{ 35, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00 } } },
{ 36, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00 } } },
{ 37, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00 } } },
{ 38, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00 } } },
{ 39, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 } } },
{ 40, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00 } } },
{ 41, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00 } } },
{ 42, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 } } },
{ 43, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00 } } },
{ 44, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x06, 0x00 } } },
{ 45, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00 } } },
{ 46, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00 } } },
{ 47, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00 } } },
{ 48, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00 } } },
{ 49, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00 } } },
{ 50, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00 } } },
{ 51, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00 } } },
{ 52, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00 } } },
{ 53, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00 } } },
{ 54, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00 } } },
{ 55, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00 } } },
{ 56, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00 } } },
{ 57, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00 } } },
{ 58, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00 } } },
{ 59, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x06, 0x00 } } },
{ 60, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00 } } },
{ 61, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00 } } },
{ 62, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00 } } },
{ 63, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00 } } },
{ 64, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00 } } },
{ 65, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00 } } },
{ 66, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00 } } },
{ 67, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00 } } },
{ 68, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00 } } },
{ 69, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00 } } },
{ 70, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00 } } },
{ 71, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00 } } },
{ 72, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00 } } },
{ 73, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 74, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00 } } },
{ 75, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00 } } },
{ 76, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00 } } },
{ 77, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00 } } },
{ 78, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00 } } },
{ 79, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00 } } },
{ 80, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00 } } },
{ 81, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00 } } },
{ 82, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00 } } },
{ 83, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00 } } },
{ 84, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 85, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00 } } },
{ 86, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 } } },
{ 87, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 } } },
{ 88, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00 } } },
{ 89, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 90, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00 } } },
{ 97, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00 } } },
{ 98, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00 } } },
{ 99, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00 } } },
{ 100, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00 } } },
{ 101, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00 } } },
{ 102, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00 } } },
{ 103, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F } } },
{ 104, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00 } } },
{ 105, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 106, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E } } },
{ 107, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00 } } },
{ 108, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 109, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00 } } },
{ 110, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00 } } },
{ 111, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00 } } },
{ 112, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F } } },
{ 113, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78 } } },
{ 114, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00 } } },
{ 115, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00 } } },
{ 116, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00 } } },
{ 117, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00 } } },
{ 118, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 } } },
{ 119, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00 } } },
{ 120, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00 } } },
{ 121, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F } } },
{ 122, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00 } } }
};
static const pxl8_font pxl8_default_font = {
pxl8_ascii_glyphs,
sizeof(pxl8_ascii_glyphs) / sizeof(pxl8_glyph),
8,
8
};
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* atlas_width, i32* atlas_height);
const pxl8_glyph* pxl8_font_find_glyph(const pxl8_font* font, u32 codepoint);
#ifdef __cplusplus
}
#endif

View file

@ -1,37 +0,0 @@
#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;
f32 fps_accumulator;
i32 fps_frame_count;
f32 fps;
pxl8_input_state input;
pxl8_input_state prev_input;
#ifndef NDEBUG
pxl8_replay* debug_replay;
#endif
bool repl_mode;
bool repl_started;
bool running;
bool script_loaded;
char script_path[256];
} pxl8_game;

View file

@ -1,504 +0,0 @@
#include "pxl8_gen.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_log.h"
#include "pxl8_rng.h"
typedef struct room_grid {
u8* cells;
i32 width;
i32 height;
} room_grid;
static bool room_grid_init(room_grid* grid, i32 width, i32 height) {
grid->width = width;
grid->height = height;
grid->cells = calloc(width * height, sizeof(u8));
return grid->cells != NULL;
}
static u8 room_grid_get(const room_grid* grid, i32 x, i32 y) {
if (x < 0 || x >= grid->width || y < 0 || y >= grid->height) {
return 1;
}
return grid->cells[y * grid->width + x];
}
static void room_grid_set(room_grid* grid, i32 x, i32 y, u8 value) {
if (x < 0 || x >= grid->width || y < 0 || y >= grid->height) {
return;
}
grid->cells[y * grid->width + x] = value;
}
static inline void compute_face_aabb(pxl8_bsp_face* face, const pxl8_bsp_vertex* verts, u32 vert_idx) {
face->aabb_min = (pxl8_vec3){1e30f, 1e30f, 1e30f};
face->aabb_max = (pxl8_vec3){-1e30f, -1e30f, -1e30f};
for (u32 i = 0; i < 4; i++) {
pxl8_vec3 v = verts[vert_idx + i].position;
if (v.x < face->aabb_min.x) face->aabb_min.x = v.x;
if (v.x > face->aabb_max.x) face->aabb_max.x = v.x;
if (v.y < face->aabb_min.y) face->aabb_min.y = v.y;
if (v.y > face->aabb_max.y) face->aabb_max.y = v.y;
if (v.z < face->aabb_min.z) face->aabb_min.z = v.z;
if (v.z > face->aabb_max.z) face->aabb_max.z = v.z;
}
}
static void room_grid_fill(room_grid* grid, u8 value) {
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
room_grid_set(grid, x, y, value);
}
}
}
static pxl8_result grid_to_bsp(pxl8_bsp* bsp, const room_grid* grid) {
i32 vertex_count = 0;
i32 face_count = 0;
i32 floor_ceiling_count = 0;
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) == 0) {
if (room_grid_get(grid, x - 1, y) == 1) face_count++;
if (room_grid_get(grid, x + 1, y) == 1) face_count++;
if (room_grid_get(grid, x, y - 1) == 1) face_count++;
if (room_grid_get(grid, x, y + 1) == 1) face_count++;
floor_ceiling_count++;
}
}
}
face_count += floor_ceiling_count * 2;
vertex_count = face_count * 4;
pxl8_debug("Cave generation: %dx%d grid -> %d faces, %d vertices",
grid->width, grid->height, face_count, vertex_count);
bsp->vertices = calloc(vertex_count, sizeof(pxl8_bsp_vertex));
bsp->faces = calloc(face_count, sizeof(pxl8_bsp_face));
bsp->planes = calloc(face_count, sizeof(pxl8_bsp_plane));
bsp->edges = calloc(vertex_count, sizeof(pxl8_bsp_edge));
bsp->surfedges = calloc(vertex_count, sizeof(i32));
if (!bsp->vertices || !bsp->faces || !bsp->planes || !bsp->edges || !bsp->surfedges) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
bsp->texinfo = NULL;
bsp->num_texinfo = 0;
i32 vert_idx = 0;
i32 face_idx = 0;
i32 edge_idx = 0;
const f32 cell_size = 64.0f;
const f32 wall_height = 128.0f;
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) == 0) {
f32 fx = (f32)x * cell_size;
f32 fy = (f32)y * cell_size;
if (room_grid_get(grid, x - 1, y) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, wall_height, fy};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, 0, fy + cell_size};
bsp->planes[face_idx].normal = (pxl8_vec3){-1, 0, 0};
bsp->planes[face_idx].dist = -fx;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
if (room_grid_get(grid, x + 1, y) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx + cell_size, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, wall_height, fy};
bsp->planes[face_idx].normal = (pxl8_vec3){1, 0, 0};
bsp->planes[face_idx].dist = fx + cell_size;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
if (room_grid_get(grid, x, y - 1) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, 0, fy};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, wall_height, fy};
bsp->planes[face_idx].normal = (pxl8_vec3){0, 0, -1};
bsp->planes[face_idx].dist = -fy;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
if (room_grid_get(grid, x, y + 1) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy + cell_size};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
bsp->planes[face_idx].normal = (pxl8_vec3){0, 0, 1};
bsp->planes[face_idx].dist = fy + cell_size;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
}
}
}
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) == 0) {
f32 fx = (f32)x * cell_size;
f32 fy = (f32)y * cell_size;
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, 0, fy + cell_size};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, 0, fy};
bsp->planes[face_idx].normal = (pxl8_vec3){0, 1, 0};
bsp->planes[face_idx].dist = 0;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, wall_height, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, wall_height, fy};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
bsp->planes[face_idx].normal = (pxl8_vec3){0, -1, 0};
bsp->planes[face_idx].dist = -wall_height;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
}
}
bsp->num_vertices = vertex_count;
bsp->num_faces = face_count;
bsp->num_planes = face_count;
bsp->num_edges = vertex_count;
bsp->num_surfedges = vertex_count;
bsp->leafs = calloc(1, sizeof(pxl8_bsp_leaf));
bsp->marksurfaces = calloc(face_count, sizeof(u16));
if (!bsp->leafs || !bsp->marksurfaces) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
bsp->num_leafs = 1;
bsp->num_marksurfaces = face_count;
bsp->leafs[0].first_marksurface = 0;
bsp->leafs[0].num_marksurfaces = face_count;
bsp->leafs[0].contents = -2;
for (i32 i = 0; i < face_count; i++) {
bsp->marksurfaces[i] = i;
}
return PXL8_OK;
}
static bool bounds_intersects(const pxl8_bounds* a, const pxl8_bounds* b) {
return !(a->x + a->w <= b->x || b->x + b->w <= a->x ||
a->y + a->h <= b->y || b->y + b->h <= a->y);
}
static void carve_corridor_h(room_grid* grid, i32 x1, i32 x2, i32 y) {
i32 start = (x1 < x2) ? x1 : x2;
i32 end = (x1 > x2) ? x1 : x2;
for (i32 x = start; x <= end; x++) {
room_grid_set(grid, x, y, 0);
room_grid_set(grid, x, y - 1, 0);
room_grid_set(grid, x, y + 1, 0);
}
}
static void carve_corridor_v(room_grid* grid, i32 y1, i32 y2, i32 x) {
i32 start = (y1 < y2) ? y1 : y2;
i32 end = (y1 > y2) ? y1 : y2;
for (i32 y = start; y <= end; y++) {
room_grid_set(grid, x, y, 0);
room_grid_set(grid, x - 1, y, 0);
room_grid_set(grid, x + 1, y, 0);
}
}
static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* params) {
pxl8_debug("procgen_rooms called: %dx%d, seed=%u, min=%d, max=%d, num=%d",
params->width, params->height, params->seed,
params->min_room_size, params->max_room_size, params->num_rooms);
pxl8_rng rng;
pxl8_rng_seed(&rng, params->seed);
room_grid grid;
if (!room_grid_init(&grid, params->width, params->height)) {
pxl8_error("Failed to allocate room grid");
return PXL8_ERROR_OUT_OF_MEMORY;
}
room_grid_fill(&grid, 1);
pxl8_bounds rooms[256];
i32 room_count = 0;
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 + (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};
bool overlaps = false;
for (i32 i = 0; i < room_count; i++) {
if (bounds_intersects(&new_room, &rooms[i])) {
overlaps = true;
break;
}
}
if (!overlaps) {
for (i32 ry = y; ry < y + h; ry++) {
for (i32 rx = x; rx < x + w; rx++) {
room_grid_set(&grid, rx, ry, 0);
}
}
if (room_count > 0) {
i32 new_cx = x + w / 2;
i32 new_cy = y + h / 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;
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 {
carve_corridor_v(&grid, prev_cy, new_cy, prev_cx);
carve_corridor_h(&grid, prev_cx, new_cx, new_cy);
}
}
rooms[room_count++] = new_room;
}
}
pxl8_debug("Room generation: %dx%d grid -> %d rooms created",
params->width, params->height, room_count);
pxl8_result result = grid_to_bsp(bsp, &grid);
free(grid.cells);
return result;
}
pxl8_result pxl8_procgen(pxl8_bsp* bsp, const pxl8_procgen_params* params) {
if (!bsp || !params) {
return PXL8_ERROR_NULL_POINTER;
}
switch (params->type) {
case PXL8_PROCGEN_ROOMS:
return procgen_rooms(bsp, params);
case PXL8_PROCGEN_TERRAIN:
pxl8_error("Terrain generation not yet implemented");
return PXL8_ERROR_NOT_INITIALIZED;
default:
pxl8_error("Unknown procgen type: %d", params->type);
return PXL8_ERROR_INVALID_ARGUMENT;
}
}
static u32 hash2d(i32 x, i32 y) {
u32 h = ((u32)x * 374761393u) + ((u32)y * 668265263u);
h ^= h >> 13;
h ^= h << 17;
h ^= h >> 5;
return h;
}
void pxl8_procgen_tex(u8* buffer, const pxl8_procgen_tex_params* params) {
if (!buffer || !params) return;
for (i32 y = 0; y < params->height; y++) {
for (i32 x = 0; x < params->width; x++) {
f32 u = (f32)x / (f32)params->width;
f32 v = (f32)y / (f32)params->height;
u8 color = params->base_color;
// Tile-based pattern (floor style)
if (params->seed == 11111) {
i32 tile_x = (i32)floorf(u * 8.0f);
i32 tile_y = (i32)floorf(v * 8.0f);
u32 h = hash2d(tile_x, tile_y);
f32 pattern = (f32)(h & 0xFF) / 255.0f;
i32 quantized = (pattern < 0.3f) ? 0 : (pattern < 0.7f) ? 1 : 2;
color = params->base_color + quantized;
// Checkerboard dither
if (((tile_x + tile_y) & 1) == 0 && (h & 0x100)) {
color = (color < 255) ? color + 1 : color;
}
}
// Large tile pattern (ceiling style)
else if (params->seed == 22222) {
i32 coarse_x = (i32)floorf(u * 2.0f);
i32 coarse_y = (i32)floorf(v * 2.0f);
u32 coarse_h = hash2d(coarse_x, coarse_y);
i32 subdivision = (coarse_h >> 8) & 0x3;
i32 tile_x, tile_y;
switch (subdivision) {
case 0: tile_x = (i32)floorf(u * 3.0f); tile_y = (i32)floorf(v * 3.0f); break;
case 1: tile_x = (i32)floorf(u * 5.0f); tile_y = (i32)floorf(v * 5.0f); break;
case 2: tile_x = (i32)floorf(u * 2.0f); tile_y = (i32)floorf(v * 4.0f); break;
default: tile_x = (i32)floorf(u * 4.0f); tile_y = (i32)floorf(v * 2.0f); break;
}
u32 h = hash2d(tile_x, tile_y);
f32 pattern = (f32)(h & 0xFF) / 255.0f;
if (pattern < 0.25f) color = params->base_color;
else if (pattern < 0.50f) color = params->base_color + 1;
else if (pattern < 0.75f) color = params->base_color + 2;
else color = params->base_color + 3;
}
// Brick pattern (wall style)
else {
f32 brick_y = floorf(v * 4.0f);
f32 offset = ((i32)brick_y & 1) ? 0.5f : 0.0f;
i32 brick_x = (i32)floorf(u * 4.0f + offset);
brick_y = (i32)brick_y;
f32 brick_u = fabsf((u * 4.0f + offset) - floorf(u * 4.0f + offset) - 0.5f);
f32 brick_v = fabsf((v * 4.0f) - floorf(v * 4.0f) - 0.5f);
u32 h = hash2d(brick_x, (i32)brick_y);
f32 noise = (f32)(h & 0xFF) / 255.0f;
// Mortar lines
if (brick_u > 0.47f || brick_v > 0.47f) {
color = params->base_color - 2;
} else {
i32 shade = (i32)(noise * 3.0f);
color = params->base_color + shade;
}
}
buffer[y * params->width + x] = color;
}
}
}

View file

@ -1,45 +0,0 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_types.h"
typedef enum pxl8_procgen_type {
PXL8_PROCGEN_ROOMS,
PXL8_PROCGEN_TERRAIN
} pxl8_procgen_type;
typedef struct pxl8_procgen_params {
pxl8_procgen_type type;
i32 width;
i32 height;
i32 depth;
u32 seed;
i32 min_room_size;
i32 max_room_size;
i32 num_rooms;
} pxl8_procgen_params;
typedef struct pxl8_procgen_tex_params {
char name[16];
u32 seed;
i32 width;
i32 height;
f32 scale;
f32 roughness;
u8 base_color;
u8 variation;
u8 max_color;
} pxl8_procgen_tex_params;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_procgen(pxl8_bsp* bsp, const pxl8_procgen_params* params);
void pxl8_procgen_tex(u8* buffer, const pxl8_procgen_tex_params* params);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load diff

View file

@ -1,86 +0,0 @@
#pragma once
#include "pxl8_hal.h"
#include "pxl8_math.h"
#include "pxl8_types.h"
typedef struct pxl8_gfx pxl8_gfx;
typedef struct pxl8_vertex {
u32 color;
pxl8_vec3 normal;
pxl8_vec3 position;
f32 u, v;
} pxl8_vertex;
typedef struct pxl8_triangle {
pxl8_vertex v[3];
u32 texture_id;
} pxl8_triangle;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_gfx* pxl8_gfx_create(const pxl8_hal* hal, void* platform_data, pxl8_pixel_mode mode, pxl8_resolution resolution);
void pxl8_gfx_destroy(pxl8_gfx* gfx);
void pxl8_gfx_present(pxl8_gfx* gfx);
void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt);
void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx);
pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx);
u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx);
u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx);
i32 pxl8_gfx_get_height(const pxl8_gfx* gfx);
u32 pxl8_gfx_get_palette_size(const pxl8_gfx* gfx);
pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx);
i32 pxl8_gfx_get_width(const pxl8_gfx* gfx);
void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom);
void pxl8_gfx_set_viewport(pxl8_gfx* gfx, pxl8_viewport vp);
pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height);
void pxl8_gfx_clear_textures(pxl8_gfx* gfx);
pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width, u32 height);
pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx);
pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path);
pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path);
i32 pxl8_gfx_add_palette_cycle(pxl8_gfx* gfx, u8 start_index, u8 end_index, f32 speed);
void pxl8_gfx_clear_palette_cycles(pxl8_gfx* gfx);
void pxl8_gfx_color_ramp(pxl8_gfx* gfx, u8 start, u8 count, u32 from_color, u32 to_color);
void pxl8_gfx_cycle_palette(pxl8_gfx* gfx, u8 start, u8 count, i32 step);
void pxl8_gfx_fade_palette(pxl8_gfx* gfx, u8 start, u8 count, f32 amount, u32 target_color);
void pxl8_gfx_interpolate_palettes(pxl8_gfx* gfx, u32* palette1, u32* palette2, u8 start, u8 count, f32 t);
void pxl8_gfx_remove_palette_cycle(pxl8_gfx* gfx, i32 cycle_id);
void pxl8_gfx_set_palette_cycle_speed(pxl8_gfx* gfx, i32 cycle_id, f32 speed);
void pxl8_gfx_swap_palette(pxl8_gfx* gfx, u8 start, u8 count, u32* new_colors);
void pxl8_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color);
void pxl8_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color);
void pxl8_clear(pxl8_gfx* gfx, u32 color);
u32 pxl8_get_pixel(pxl8_gfx* gfx, i32 x, i32 y);
void pxl8_line(pxl8_gfx* gfx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color);
void pxl8_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color);
void pxl8_rect(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);
void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y);
void pxl8_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color);
void pxl8_3d_clear_zbuffer(pxl8_gfx* gfx);
void pxl8_3d_draw_line_3d(pxl8_gfx* gfx, pxl8_vec3 p0, pxl8_vec3 p1, u32 color);
void pxl8_3d_draw_triangle(pxl8_gfx* gfx, pxl8_triangle tri);
void pxl8_3d_draw_triangle_raw(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, u32 color);
void pxl8_3d_draw_triangle_textured(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, f32 u0, f32 v0f, f32 u1, f32 v1f, f32 u2, f32 v2f, u32 texture_id);
const pxl8_frustum* pxl8_3d_get_frustum(pxl8_gfx* gfx);
void pxl8_3d_set_affine_textures(pxl8_gfx* gfx, bool affine);
void pxl8_3d_set_backface_culling(pxl8_gfx* gfx, bool culling);
void pxl8_3d_set_model(pxl8_gfx* gfx, pxl8_mat4 mat);
void pxl8_3d_set_projection(pxl8_gfx* gfx, pxl8_mat4 mat);
void pxl8_3d_set_view(pxl8_gfx* gfx, pxl8_mat4 mat);
void pxl8_3d_set_wireframe(pxl8_gfx* gfx, bool wireframe);
#ifdef __cplusplus
}
#endif

View file

@ -1,131 +0,0 @@
#include "pxl8_gui.h"
#include <stdlib.h>
#include <string.h>
pxl8_gui_state* pxl8_gui_state_create(void) {
pxl8_gui_state* state = (pxl8_gui_state*)malloc(sizeof(pxl8_gui_state));
if (!state) return NULL;
memset(state, 0, sizeof(pxl8_gui_state));
return state;
}
void pxl8_gui_state_destroy(pxl8_gui_state* state) {
if (!state) return;
free(state);
}
void pxl8_gui_begin_frame(pxl8_gui_state* state) {
if (!state) return;
state->hot_id = 0;
}
void pxl8_gui_end_frame(pxl8_gui_state* state) {
if (!state) return;
if (!state->cursor_down) {
state->active_id = 0;
}
state->cursor_clicked = false;
}
void pxl8_gui_cursor_move(pxl8_gui_state* state, i32 x, i32 y) {
if (!state) return;
state->cursor_x = x;
state->cursor_y = y;
}
void pxl8_gui_cursor_down(pxl8_gui_state* state) {
if (!state) return;
state->cursor_down = true;
}
void pxl8_gui_cursor_up(pxl8_gui_state* state) {
if (!state) return;
state->cursor_down = false;
state->cursor_clicked = true;
}
static bool is_cursor_over(const pxl8_gui_state* state, i32 x, i32 y, i32 w, i32 h) {
return state->cursor_x >= x && state->cursor_x < (x + w) &&
state->cursor_y >= y && state->cursor_y < (y + h);
}
bool pxl8_gui_button(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, const char* label) {
if (!state || !gfx || !label) return false;
bool cursor_over = is_cursor_over(state, x, y, w, h);
bool is_hot = (state->hot_id == id);
bool is_active = (state->active_id == id);
if (cursor_over) {
state->hot_id = id;
}
if (cursor_over && state->cursor_down && state->active_id == 0) {
state->active_id = id;
}
bool clicked = is_active && state->cursor_clicked && cursor_over;
if (clicked) {
state->active_id = 0;
}
u8 bg_color;
u8 border_color;
i32 offset_x = 0;
i32 offset_y = 0;
if (is_active) {
bg_color = 4;
border_color = 3;
offset_x = 1;
offset_y = 1;
} else if (is_hot || cursor_over) {
bg_color = 4;
border_color = 8;
} else {
bg_color = 3;
border_color = 4;
}
pxl8_rect_fill(gfx, x, y, w, h, bg_color);
pxl8_rect(gfx, x, y, w, h, border_color);
i32 text_len = (i32)strlen(label);
i32 text_x = x + (w / 2) - ((text_len * 8) / 2) + offset_x;
i32 text_y = y + (h / 2) - 5 + offset_y;
pxl8_text(gfx, label, text_x, text_y, 6);
return clicked;
}
void pxl8_gui_window(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, const char* title) {
if (!gfx || !title) return;
pxl8_rect_fill(gfx, x, y, w, 28, 1);
pxl8_rect_fill(gfx, x, y + 28, w, h - 28, 2);
pxl8_rect(gfx, x, y, w, h, 4);
pxl8_rect_fill(gfx, x, y + 28, w, 1, 4);
i32 title_x = x + 10;
i32 title_y = y + (28 / 2) - 5;
pxl8_text(gfx, title, title_x, title_y, 8);
}
void pxl8_gui_label(pxl8_gfx* gfx, i32 x, i32 y, const char* text, u8 color) {
if (!gfx || !text) return;
pxl8_text(gfx, text, x, y, color);
}
bool pxl8_gui_is_hovering(const pxl8_gui_state* state) {
if (!state) return false;
return state->hot_id != 0;
}
void pxl8_gui_get_cursor_pos(const pxl8_gui_state* state, i32* x, i32* y) {
if (!state) return;
if (x) *x = state->cursor_x;
if (y) *y = state->cursor_y;
}

View file

@ -1,38 +0,0 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_types.h"
typedef struct {
i32 cursor_x;
i32 cursor_y;
bool cursor_down;
bool cursor_clicked;
u32 hot_id;
u32 active_id;
} pxl8_gui_state;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_gui_state* pxl8_gui_state_create(void);
void pxl8_gui_state_destroy(pxl8_gui_state* state);
void pxl8_gui_get_cursor_pos(const pxl8_gui_state* state, i32* x, i32* y);
bool pxl8_gui_is_hovering(const pxl8_gui_state* state);
void pxl8_gui_begin_frame(pxl8_gui_state* state);
void pxl8_gui_end_frame(pxl8_gui_state* state);
void pxl8_gui_cursor_down(pxl8_gui_state* state);
void pxl8_gui_cursor_move(pxl8_gui_state* state, i32 x, i32 y);
void pxl8_gui_cursor_up(pxl8_gui_state* state);
bool pxl8_gui_button(pxl8_gui_state* state, pxl8_gfx* gfx, u32 id, i32 x, i32 y, i32 w, i32 h, const char* label);
void pxl8_gui_label(pxl8_gfx* gfx, i32 x, i32 y, const char* text, u8 color);
void pxl8_gui_window(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, const char* title);
#ifdef __cplusplus
}
#endif

View file

@ -1,23 +0,0 @@
#pragma once
#include "pxl8_types.h"
typedef struct pxl8_hal {
void* (*create)(i32 render_w, i32 render_h,
const char* title, i32 win_w, i32 win_h);
void (*destroy)(void* platform_data);
u64 (*get_ticks)(void);
void (*center_cursor)(void* platform_data);
void (*present)(void* platform_data);
void (*set_cursor)(void* platform_data, u32 cursor);
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;

View file

@ -1,221 +0,0 @@
#include "pxl8_io.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_cart.h"
#include "pxl8_types.h"
static inline char pxl8_to_lower(char c) {
return (c >= 'A' && c <= 'Z') ? c + 32 : c;
}
pxl8_result pxl8_io_read_file(const char* path, char** content, size_t* size) {
if (!path || !content || !size) return PXL8_ERROR_NULL_POINTER;
pxl8_cart* cart = pxl8_get_cart();
if (cart && pxl8_cart_is_packed(cart)) {
u8* data = NULL;
u32 cart_size = 0;
pxl8_result result = pxl8_cart_read_file(cart, path, &data, &cart_size);
if (result == PXL8_OK) {
*content = realloc(data, cart_size + 1);
if (!*content) {
pxl8_cart_free_file(data);
return PXL8_ERROR_OUT_OF_MEMORY;
}
(*content)[cart_size] = '\0';
*size = cart_size;
return PXL8_OK;
}
}
FILE* file = fopen(path, "rb");
if (!file) {
return PXL8_ERROR_FILE_NOT_FOUND;
}
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
fseek(file, 0, SEEK_SET);
if (file_size < 0) {
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
*content = malloc(file_size + 1);
if (!*content) {
fclose(file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
size_t bytes_read = fread(*content, 1, file_size, file);
(*content)[bytes_read] = '\0';
*size = bytes_read;
fclose(file);
return PXL8_OK;
}
pxl8_result pxl8_io_write_file(const char* path, const char* content, size_t size) {
if (!path || !content) return PXL8_ERROR_NULL_POINTER;
FILE* file = fopen(path, "wb");
if (!file) {
return PXL8_ERROR_SYSTEM_FAILURE;
}
size_t bytes_written = fwrite(content, 1, size, file);
fclose(file);
return (bytes_written == size) ? PXL8_OK : PXL8_ERROR_SYSTEM_FAILURE;
}
pxl8_result pxl8_io_read_binary_file(const char* path, u8** data, size_t* size) {
return pxl8_io_read_file(path, (char**)data, size);
}
pxl8_result pxl8_io_write_binary_file(const char* path, const u8* data, size_t size) {
return pxl8_io_write_file(path, (const char*)data, size);
}
bool pxl8_io_file_exists(const char* path) {
if (!path) return false;
struct stat st;
return stat(path, &st) == 0;
}
f64 pxl8_io_get_file_modified_time(const char* path) {
if (!path) return 0.0;
struct stat st;
if (stat(path, &st) == 0) {
return st.st_mtime;
}
return 0.0;
}
pxl8_result pxl8_io_create_directory(const char* path) {
if (!path) return PXL8_ERROR_NULL_POINTER;
#ifdef _WIN32
if (mkdir(path) != 0) {
#else
if (mkdir(path, 0755) != 0) {
#endif
return PXL8_ERROR_SYSTEM_FAILURE;
}
return PXL8_OK;
}
void pxl8_io_free_file_content(char* content) {
if (content) {
free(content);
}
}
void pxl8_io_free_binary_data(u8* data) {
if (data) {
free(data);
}
}
static i32 pxl8_key_code(const char* key_name) {
if (!key_name || !key_name[0]) return 0;
typedef struct { const char* name; i32 code; } KeyMapping;
static const KeyMapping keys[] = {
{"a", 4}, {"b", 5}, {"c", 6}, {"d", 7}, {"e", 8}, {"f", 9}, {"g", 10}, {"h", 11},
{"i", 12}, {"j", 13}, {"k", 14}, {"l", 15}, {"m", 16}, {"n", 17}, {"o", 18}, {"p", 19},
{"q", 20}, {"r", 21}, {"s", 22}, {"t", 23}, {"u", 24}, {"v", 25}, {"w", 26}, {"x", 27},
{"y", 28}, {"z", 29},
{"1", 30}, {"2", 31}, {"3", 32}, {"4", 33}, {"5", 34},
{"6", 35}, {"7", 36}, {"8", 37}, {"9", 38}, {"0", 39},
{"return", 40}, {"escape", 41}, {"backspace", 42}, {"tab", 43}, {"space", 44},
{"-", 45}, {"=", 46}, {"`", 53},
{"left", 80}, {"right", 79}, {"up", 82}, {"down", 81},
{"f1", 58}, {"f2", 59}, {"f3", 60}, {"f4", 61}, {"f5", 62}, {"f6", 63},
{"f7", 64}, {"f8", 65}, {"f9", 66}, {"f10", 67}, {"f11", 68}, {"f12", 69},
{"capslock", 57}, {"lshift", 225}, {"rshift", 229},
{"lctrl", 224}, {"rctrl", 228}, {"lalt", 226}, {"ralt", 230},
{NULL, 0}
};
char lower_name[64];
size_t i;
for (i = 0; i < sizeof(lower_name) - 1 && key_name[i]; i++) {
lower_name[i] = pxl8_to_lower(key_name[i]);
}
lower_name[i] = '\0';
for (i = 0; keys[i].name; i++) {
if (strcmp(lower_name, keys[i].name) == 0) {
return keys[i].code;
}
}
return 0;
}
bool pxl8_key_down(const pxl8_input_state* input, const char* key_name) {
if (!input) return false;
i32 key = pxl8_key_code(key_name);
if (key < 0 || key >= PXL8_MAX_KEYS) return false;
return input->keys_down[key];
}
bool pxl8_key_pressed(const pxl8_input_state* input, const char* key_name) {
if (!input) return false;
i32 key = pxl8_key_code(key_name);
if (key < 0 || key >= PXL8_MAX_KEYS) return false;
return input->keys_pressed[key];
}
bool pxl8_key_released(const pxl8_input_state* input, const char* key_name) {
if (!input) return false;
i32 key = pxl8_key_code(key_name);
if (key < 0 || key >= PXL8_MAX_KEYS) return false;
return input->keys_released[key];
}
i32 pxl8_mouse_wheel_x(const pxl8_input_state* input) {
if (!input) return 0;
return input->mouse_wheel_x;
}
i32 pxl8_mouse_wheel_y(const pxl8_input_state* input) {
if (!input) return 0;
return input->mouse_wheel_y;
}
bool pxl8_mouse_pressed(const pxl8_input_state* input, i32 button) {
if (!input || button < 1 || button > 3) return false;
return input->mouse_buttons_pressed[button - 1];
}
bool pxl8_mouse_released(const pxl8_input_state* input, i32 button) {
if (!input || button < 1 || button > 3) return false;
return input->mouse_buttons_released[button - 1];
}
i32 pxl8_mouse_x(const pxl8_input_state* input) {
if (!input) return 0;
return input->mouse_x;
}
i32 pxl8_mouse_y(const pxl8_input_state* input) {
if (!input) return 0;
return input->mouse_y;
}
i32 pxl8_mouse_dx(const pxl8_input_state* input) {
if (!input) return 0;
return input->mouse_dx;
}
i32 pxl8_mouse_dy(const pxl8_input_state* input) {
if (!input) return 0;
return input->mouse_dy;
}

View file

@ -1,126 +0,0 @@
#pragma once
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include "pxl8_types.h"
typedef struct {
const u8* bytes;
u32 offset;
u32 size;
bool overflow;
} pxl8_stream;
static inline pxl8_stream pxl8_stream_create(const u8* bytes, u32 size) {
return (pxl8_stream){
.bytes = bytes,
.offset = 0,
.size = size,
.overflow = false
};
}
static inline bool pxl8_stream_can_read(const pxl8_stream* stream, u32 count) {
return !stream->overflow && stream->offset + count <= stream->size;
}
static inline bool pxl8_stream_has_overflow(const pxl8_stream* stream) {
return stream->overflow;
}
static inline void pxl8_stream_seek(pxl8_stream* stream, u32 offset) {
stream->offset = offset;
}
static inline u32 pxl8_stream_position(const pxl8_stream* stream) {
return stream->offset;
}
static inline u8 pxl8_read_u8(pxl8_stream* stream) {
if (stream->offset + 1 > stream->size) { stream->overflow = true; return 0; }
return stream->bytes[stream->offset++];
}
static inline u16 pxl8_read_u16(pxl8_stream* stream) {
if (stream->offset + 2 > stream->size) { stream->overflow = true; return 0; }
u16 val = (u16)stream->bytes[stream->offset] | ((u16)stream->bytes[stream->offset + 1] << 8);
stream->offset += 2;
return val;
}
static inline u32 pxl8_read_u32(pxl8_stream* stream) {
if (stream->offset + 4 > stream->size) { stream->overflow = true; return 0; }
u32 val = (u32)stream->bytes[stream->offset] |
((u32)stream->bytes[stream->offset + 1] << 8) |
((u32)stream->bytes[stream->offset + 2] << 16) |
((u32)stream->bytes[stream->offset + 3] << 24);
stream->offset += 4;
return val;
}
static inline i16 pxl8_read_i16(pxl8_stream* stream) {
return (i16)pxl8_read_u16(stream);
}
static inline i32 pxl8_read_i32(pxl8_stream* stream) {
return (i32)pxl8_read_u32(stream);
}
static inline f32 pxl8_read_f32(pxl8_stream* stream) {
u32 val = pxl8_read_u32(stream);
f32 result;
memcpy(&result, &val, sizeof(f32));
return result;
}
static inline void pxl8_read_bytes(pxl8_stream* stream, void* dest, u32 count) {
if (stream->offset + count > stream->size) { stream->overflow = true; return; }
for (u32 i = 0; i < count; i++) {
((u8*)dest)[i] = stream->bytes[stream->offset++];
}
}
static inline void pxl8_skip_bytes(pxl8_stream* stream, u32 count) {
if (stream->offset + count > stream->size) { stream->overflow = true; return; }
stream->offset += count;
}
static inline const u8* pxl8_read_ptr(pxl8_stream* stream, u32 count) {
if (stream->offset + count > stream->size) { stream->overflow = true; return NULL; }
const u8* ptr = &stream->bytes[stream->offset];
stream->offset += count;
return ptr;
}
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_io_create_directory(const char* path);
bool pxl8_io_file_exists(const char* path);
void pxl8_io_free_binary_data(u8* data);
void pxl8_io_free_file_content(char* content);
f64 pxl8_io_get_file_modified_time(const char* path);
pxl8_result pxl8_io_read_binary_file(const char* path, u8** data, size_t* size);
pxl8_result pxl8_io_read_file(const char* path, char** content, size_t* size);
pxl8_result pxl8_io_write_binary_file(const char* path, const u8* data, size_t size);
pxl8_result pxl8_io_write_file(const char* path, const char* content, size_t size);
bool pxl8_key_down(const pxl8_input_state* input, const char* key_name);
bool pxl8_key_pressed(const pxl8_input_state* input, const char* key_name);
bool pxl8_key_released(const pxl8_input_state* input, const char* key_name);
i32 pxl8_mouse_dx(const pxl8_input_state* input);
i32 pxl8_mouse_dy(const pxl8_input_state* input);
bool pxl8_mouse_pressed(const pxl8_input_state* input, i32 button);
bool pxl8_mouse_released(const pxl8_input_state* input, i32 button);
i32 pxl8_mouse_wheel_x(const pxl8_input_state* input);
i32 pxl8_mouse_wheel_y(const pxl8_input_state* input);
i32 pxl8_mouse_x(const pxl8_input_state* input);
i32 pxl8_mouse_y(const pxl8_input_state* input);
#ifdef __cplusplus
}
#endif

View file

@ -1,113 +0,0 @@
#include "pxl8_log.h"
#include "pxl8_repl.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define PXL8_LOG_COLOR_ERROR "\033[38;2;251;73;52m"
#define PXL8_LOG_COLOR_WARN "\033[38;2;250;189;47m"
#define PXL8_LOG_COLOR_INFO "\033[38;2;184;187;38m"
#define PXL8_LOG_COLOR_DEBUG "\033[38;2;131;165;152m"
#define PXL8_LOG_COLOR_TRACE "\033[38;2;211;134;155m"
#define PXL8_LOG_COLOR_RESET "\033[0m"
static pxl8_log* g_log = NULL;
void pxl8_log_init(pxl8_log* log) {
g_log = log;
g_log->level = PXL8_LOG_LEVEL_DEBUG;
const char* env_level = getenv("PXL8_LOG_LEVEL");
if (env_level) {
if (strcmp(env_level, "trace") == 0) g_log->level = PXL8_LOG_LEVEL_TRACE;
else if (strcmp(env_level, "debug") == 0) g_log->level = PXL8_LOG_LEVEL_DEBUG;
else if (strcmp(env_level, "info") == 0) g_log->level = PXL8_LOG_LEVEL_INFO;
else if (strcmp(env_level, "warn") == 0) g_log->level = PXL8_LOG_LEVEL_WARN;
else if (strcmp(env_level, "error") == 0) g_log->level = PXL8_LOG_LEVEL_ERROR;
}
}
void pxl8_log_set_level(pxl8_log_level level) {
if (g_log) g_log->level = level;
}
static void log_timestamp(char* buffer, size_t size) {
time_t now = time(NULL);
struct tm* tm_info = localtime(&now);
strftime(buffer, size, "%H:%M:%S", tm_info);
}
static void log_output(const char* color, const char* level,
const char* file, int line, const char* fmt, va_list args) {
char buffer[4096];
char timestamp[16];
log_timestamp(timestamp, sizeof(timestamp));
int pos = 0;
if (file) {
pos = snprintf(buffer, sizeof(buffer), "%s[%s %s]" PXL8_LOG_COLOR_RESET " %s:%d: ",
color, timestamp, level, file, line);
} else {
pos = snprintf(buffer, sizeof(buffer), "%s[%s %s]" PXL8_LOG_COLOR_RESET " ",
color, timestamp, level);
}
if (pos > 0 && pos < (int)sizeof(buffer)) {
vsnprintf(buffer + pos, sizeof(buffer) - pos, fmt, args);
}
strncat(buffer, "\n", sizeof(buffer) - strlen(buffer) - 1);
if (!pxl8_repl_push_log(buffer)) {
printf("%s", buffer);
fflush(stdout);
}
}
void pxl8_log_write_trace(const char* file, int line, const char* fmt, ...) {
if (!g_log || g_log->level > PXL8_LOG_LEVEL_TRACE) return;
va_list args;
va_start(args, fmt);
log_output(PXL8_LOG_COLOR_TRACE, "TRACE", file, line, fmt, args);
va_end(args);
}
void pxl8_log_write_debug(const char* file, int line, const char* fmt, ...) {
if (!g_log || g_log->level > PXL8_LOG_LEVEL_DEBUG) return;
va_list args;
va_start(args, fmt);
log_output(PXL8_LOG_COLOR_DEBUG, "DEBUG", file, line, fmt, args);
va_end(args);
}
void pxl8_log_write_info(const char* fmt, ...) {
if (!g_log || g_log->level > PXL8_LOG_LEVEL_INFO) return;
va_list args;
va_start(args, fmt);
log_output(PXL8_LOG_COLOR_INFO, "INFO", NULL, 0, fmt, args);
va_end(args);
}
void pxl8_log_write_warn(const char* file, int line, const char* fmt, ...) {
if (!g_log || g_log->level > PXL8_LOG_LEVEL_WARN) return;
va_list args;
va_start(args, fmt);
log_output(PXL8_LOG_COLOR_WARN, "WARN", file, line, fmt, args);
va_end(args);
}
void pxl8_log_write_error(const char* file, int line, const char* fmt, ...) {
if (!g_log || g_log->level > PXL8_LOG_LEVEL_ERROR) return;
va_list args;
va_start(args, fmt);
log_output(PXL8_LOG_COLOR_ERROR, "ERROR", file, line, fmt, args);
va_end(args);
}

View file

@ -1,31 +0,0 @@
#pragma once
#include <stdbool.h>
typedef enum {
PXL8_LOG_LEVEL_TRACE = 0,
PXL8_LOG_LEVEL_DEBUG = 1,
PXL8_LOG_LEVEL_INFO = 2,
PXL8_LOG_LEVEL_WARN = 3,
PXL8_LOG_LEVEL_ERROR = 4,
} pxl8_log_level;
typedef struct pxl8_log {
pxl8_log_level level;
} pxl8_log;
void pxl8_log_init(pxl8_log* log);
void pxl8_log_set_level(pxl8_log_level level);
void pxl8_log_write_trace(const char* file, int line, const char* fmt, ...);
void pxl8_log_write_debug(const char* file, int line, const char* fmt, ...);
void pxl8_log_write_info(const char* fmt, ...);
void pxl8_log_write_warn(const char* file, int line, const char* fmt, ...);
void pxl8_log_write_error(const char* file, int line, const char* fmt, ...);
#define pxl8_trace(...) pxl8_log_write_trace(__FILE__, __LINE__, __VA_ARGS__)
#define pxl8_debug(...) pxl8_log_write_debug(__FILE__, __LINE__, __VA_ARGS__)
#define pxl8_info(...) pxl8_log_write_info(__VA_ARGS__)
#define pxl8_warn(...) pxl8_log_write_warn(__FILE__, __LINE__, __VA_ARGS__)
#define pxl8_error(...) pxl8_log_write_error(__FILE__, __LINE__, __VA_ARGS__)
#define pxl8_script_error(...) pxl8_log_write_error(NULL, 0, __VA_ARGS__)

View file

@ -1,18 +0,0 @@
#pragma once
#include <string.h>
#ifndef pxl8_min
#define pxl8_min(a, b) ((a) < (b) ? (a) : (b))
#endif
#ifndef pxl8_max
#define pxl8_max(a, b) ((a) > (b) ? (a) : (b))
#endif
#ifndef pxl8_strncpy
#define pxl8_strncpy(dst, src, size) do { \
strncpy((dst), (src), (size) - 1); \
(dst)[(size) - 1] = '\0'; \
} while (0)
#endif

View file

@ -1,294 +0,0 @@
#include "pxl8_math.h"
pxl8_vec2 pxl8_vec2_add(pxl8_vec2 a, pxl8_vec2 b) {
return (pxl8_vec2){
.x = a.x + b.x,
.y = a.y + b.y,
};
}
pxl8_vec2 pxl8_vec2_sub(pxl8_vec2 a, pxl8_vec2 b) {
return (pxl8_vec2){
.x = a.x - b.x,
.y = a.y - b.y,
};
}
pxl8_vec2 pxl8_vec2_scale(pxl8_vec2 v, f32 s) {
return (pxl8_vec2){
.x = v.x * s,
.y = v.y * s,
};
}
f32 pxl8_vec2_dot(pxl8_vec2 a, pxl8_vec2 b) {
return a.x * b.x + a.y * b.y;
}
f32 pxl8_vec2_length(pxl8_vec2 v) {
return sqrtf(v.x * v.x + v.y * v.y);
}
pxl8_vec2 pxl8_vec2_normalize(pxl8_vec2 v) {
f32 len = pxl8_vec2_length(v);
if (len < 1e-6f) return (pxl8_vec2){0};
return pxl8_vec2_scale(v, 1.0f / len);
}
pxl8_vec3 pxl8_vec3_add(pxl8_vec3 a, pxl8_vec3 b) {
return (pxl8_vec3){
.x = a.x + b.x,
.y = a.y + b.y,
.z = a.z + b.z,
};
}
pxl8_vec3 pxl8_vec3_sub(pxl8_vec3 a, pxl8_vec3 b) {
return (pxl8_vec3){
.x = a.x - b.x,
.y = a.y - b.y,
.z = a.z - b.z,
};
}
pxl8_vec3 pxl8_vec3_scale(pxl8_vec3 v, f32 s) {
return (pxl8_vec3){
.x = v.x * s,
.y = v.y * s,
.z = v.z * s,
};
}
f32 pxl8_vec3_dot(pxl8_vec3 a, pxl8_vec3 b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
pxl8_vec3 pxl8_vec3_cross(pxl8_vec3 a, pxl8_vec3 b) {
return (pxl8_vec3){
.x = a.y * b.z - a.z * b.y,
.y = a.z * b.x - a.x * b.z,
.z = a.x * b.y - a.y * b.x,
};
}
f32 pxl8_vec3_length(pxl8_vec3 v) {
return sqrtf(pxl8_vec3_dot(v, v));
}
pxl8_vec3 pxl8_vec3_normalize(pxl8_vec3 v) {
f32 len = pxl8_vec3_length(v);
if (len < 1e-6f) return (pxl8_vec3){0};
return pxl8_vec3_scale(v, 1.0f / len);
}
pxl8_mat4 pxl8_mat4_identity(void) {
pxl8_mat4 mat = {0};
mat.m[0] = mat.m[5] = mat.m[10] = mat.m[15] = 1.0f;
return mat;
}
pxl8_mat4 pxl8_mat4_multiply(pxl8_mat4 a, pxl8_mat4 b) {
pxl8_mat4 mat = {0};
for (i32 col = 0; col < 4; col++) {
for (i32 row = 0; row < 4; row++) {
mat.m[col * 4 + row] =
a.m[0 * 4 + row] * b.m[col * 4 + 0] +
a.m[1 * 4 + row] * b.m[col * 4 + 1] +
a.m[2 * 4 + row] * b.m[col * 4 + 2] +
a.m[3 * 4 + row] * b.m[col * 4 + 3];
}
}
return mat;
}
pxl8_vec4 pxl8_mat4_multiply_vec4(pxl8_mat4 m, pxl8_vec4 v) {
return (pxl8_vec4){
.x = m.m[0] * v.x + m.m[4] * v.y + m.m[8] * v.z + m.m[12] * v.w,
.y = m.m[1] * v.x + m.m[5] * v.y + m.m[9] * v.z + m.m[13] * v.w,
.z = m.m[2] * v.x + m.m[6] * v.y + m.m[10] * v.z + m.m[14] * v.w,
.w = m.m[3] * v.x + m.m[7] * v.y + m.m[11] * v.z + m.m[15] * v.w,
};
}
pxl8_mat4 pxl8_mat4_translate(f32 x, f32 y, f32 z) {
pxl8_mat4 mat = pxl8_mat4_identity();
mat.m[12] = x;
mat.m[13] = y;
mat.m[14] = z;
return mat;
}
pxl8_mat4 pxl8_mat4_rotate_x(f32 angle) {
pxl8_mat4 mat = pxl8_mat4_identity();
f32 c = cosf(angle);
f32 s = sinf(angle);
mat.m[5] = c;
mat.m[9] = -s;
mat.m[6] = s;
mat.m[10] = c;
return mat;
}
pxl8_mat4 pxl8_mat4_rotate_y(f32 angle) {
pxl8_mat4 mat = pxl8_mat4_identity();
f32 c = cosf(angle);
f32 s = sinf(angle);
mat.m[0] = c;
mat.m[8] = s;
mat.m[2] = -s;
mat.m[10] = c;
return mat;
}
pxl8_mat4 pxl8_mat4_rotate_z(f32 angle) {
pxl8_mat4 mat = pxl8_mat4_identity();
f32 c = cosf(angle);
f32 s = sinf(angle);
mat.m[0] = c;
mat.m[4] = -s;
mat.m[1] = s;
mat.m[5] = c;
return mat;
}
pxl8_mat4 pxl8_mat4_scale(f32 x, f32 y, f32 z) {
pxl8_mat4 mat = pxl8_mat4_identity();
mat.m[0] = x;
mat.m[5] = y;
mat.m[10] = z;
return mat;
}
pxl8_mat4 pxl8_mat4_ortho(f32 left, f32 right, f32 bottom, f32 top, f32 near, f32 far) {
pxl8_mat4 mat = {0};
mat.m[0] = 2.0f / (right - left);
mat.m[5] = 2.0f / (top - bottom);
mat.m[10] = -2.0f / (far - near);
mat.m[12] = -(right + left) / (right - left);
mat.m[13] = -(top + bottom) / (top - bottom);
mat.m[14] = -(far + near) / (far - near);
mat.m[15] = 1.0f;
return mat;
}
pxl8_mat4 pxl8_mat4_perspective(f32 fov, f32 aspect, f32 near, f32 far) {
pxl8_mat4 mat = {0};
f32 tan_half_fov = tanf(fov / 2.0f);
mat.m[0] = 1.0f / (aspect * tan_half_fov);
mat.m[5] = 1.0f / tan_half_fov;
mat.m[10] = -(far + near) / (far - near);
mat.m[14] = -(2.0f * far * near) / (far - near);
mat.m[11] = -1.0f;
return mat;
}
pxl8_mat4 pxl8_mat4_lookat(pxl8_vec3 eye, pxl8_vec3 center, pxl8_vec3 up) {
pxl8_mat4 mat = pxl8_mat4_identity();
pxl8_vec3 f = pxl8_vec3_normalize(pxl8_vec3_sub(center, eye));
pxl8_vec3 s = pxl8_vec3_normalize(pxl8_vec3_cross(f, up));
pxl8_vec3 u = pxl8_vec3_cross(s, f);
mat.m[0] = s.x;
mat.m[4] = s.y;
mat.m[8] = s.z;
mat.m[1] = u.x;
mat.m[5] = u.y;
mat.m[9] = u.z;
mat.m[2] = -f.x;
mat.m[6] = -f.y;
mat.m[10] = -f.z;
mat.m[12] = -pxl8_vec3_dot(s, eye);
mat.m[13] = -pxl8_vec3_dot(u, eye);
mat.m[14] = pxl8_vec3_dot(f, eye);
return mat;
}
pxl8_frustum pxl8_frustum_from_matrix(pxl8_mat4 vp) {
pxl8_frustum frustum;
const f32* m = vp.m;
frustum.planes[0].normal.x = m[3] - m[0];
frustum.planes[0].normal.y = m[7] - m[4];
frustum.planes[0].normal.z = m[11] - m[8];
frustum.planes[0].distance = m[15] - m[12];
frustum.planes[1].normal.x = m[3] + m[0];
frustum.planes[1].normal.y = m[7] + m[4];
frustum.planes[1].normal.z = m[11] + m[8];
frustum.planes[1].distance = m[15] + m[12];
frustum.planes[2].normal.x = m[3] + m[1];
frustum.planes[2].normal.y = m[7] + m[5];
frustum.planes[2].normal.z = m[11] + m[9];
frustum.planes[2].distance = m[15] + m[13];
frustum.planes[3].normal.x = m[3] - m[1];
frustum.planes[3].normal.y = m[7] - m[5];
frustum.planes[3].normal.z = m[11] - m[9];
frustum.planes[3].distance = m[15] - m[13];
frustum.planes[4].normal.x = m[3] - m[2];
frustum.planes[4].normal.y = m[7] - m[6];
frustum.planes[4].normal.z = m[11] - m[10];
frustum.planes[4].distance = m[15] - m[14];
frustum.planes[5].normal.x = m[3] + m[2];
frustum.planes[5].normal.y = m[7] + m[6];
frustum.planes[5].normal.z = m[11] + m[10];
frustum.planes[5].distance = m[15] + m[14];
for (i32 i = 0; i < 6; i++) {
f32 len = pxl8_vec3_length(frustum.planes[i].normal);
if (len > 1e-6f) {
f32 inv_len = 1.0f / len;
frustum.planes[i].normal = pxl8_vec3_scale(frustum.planes[i].normal, inv_len);
frustum.planes[i].distance *= inv_len;
}
}
return frustum;
}
bool pxl8_frustum_test_aabb(const pxl8_frustum* frustum, pxl8_vec3 min, pxl8_vec3 max) {
for (i32 i = 0; i < 6; i++) {
pxl8_vec3 normal = frustum->planes[i].normal;
f32 d = frustum->planes[i].distance;
pxl8_vec3 p_vertex = {
(normal.x >= 0.0f) ? max.x : min.x,
(normal.y >= 0.0f) ? max.y : min.y,
(normal.z >= 0.0f) ? max.z : min.z
};
f32 p_dist = pxl8_vec3_dot(normal, p_vertex) + d;
if (p_dist < 0.0f) {
return false;
}
}
return true;
}

View file

@ -1,71 +0,0 @@
#pragma once
#include <math.h>
#include "pxl8_types.h"
#define PXL8_PI 3.14159265358979323846f
#define PXL8_TAU (PXL8_PI * 2.0f)
typedef struct pxl8_vec2 {
f32 x, y;
} pxl8_vec2;
typedef struct pxl8_vec3 {
f32 x, y, z;
} pxl8_vec3;
typedef struct pxl8_vec4 {
f32 x, y, z, w;
} pxl8_vec4;
typedef struct pxl8_mat4 {
f32 m[16];
} pxl8_mat4;
typedef struct pxl8_plane {
pxl8_vec3 normal;
f32 distance;
} pxl8_plane;
typedef struct pxl8_frustum {
pxl8_plane planes[6];
} pxl8_frustum;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_vec2 pxl8_vec2_add(pxl8_vec2 a, pxl8_vec2 b);
f32 pxl8_vec2_dot(pxl8_vec2 a, pxl8_vec2 b);
f32 pxl8_vec2_length(pxl8_vec2 v);
pxl8_vec2 pxl8_vec2_normalize(pxl8_vec2 v);
pxl8_vec2 pxl8_vec2_scale(pxl8_vec2 v, f32 s);
pxl8_vec2 pxl8_vec2_sub(pxl8_vec2 a, pxl8_vec2 b);
pxl8_vec3 pxl8_vec3_add(pxl8_vec3 a, pxl8_vec3 b);
pxl8_vec3 pxl8_vec3_cross(pxl8_vec3 a, pxl8_vec3 b);
f32 pxl8_vec3_dot(pxl8_vec3 a, pxl8_vec3 b);
f32 pxl8_vec3_length(pxl8_vec3 v);
pxl8_vec3 pxl8_vec3_normalize(pxl8_vec3 v);
pxl8_vec3 pxl8_vec3_scale(pxl8_vec3 v, f32 s);
pxl8_vec3 pxl8_vec3_sub(pxl8_vec3 a, pxl8_vec3 b);
pxl8_mat4 pxl8_mat4_identity(void);
pxl8_mat4 pxl8_mat4_lookat(pxl8_vec3 eye, pxl8_vec3 center, pxl8_vec3 up);
pxl8_mat4 pxl8_mat4_multiply(pxl8_mat4 a, pxl8_mat4 b);
pxl8_vec4 pxl8_mat4_multiply_vec4(pxl8_mat4 m, pxl8_vec4 v);
pxl8_mat4 pxl8_mat4_ortho(f32 left, f32 right, f32 bottom, f32 top, f32 near, f32 far);
pxl8_mat4 pxl8_mat4_perspective(f32 fov, f32 aspect, f32 near, f32 far);
pxl8_mat4 pxl8_mat4_rotate_x(f32 angle);
pxl8_mat4 pxl8_mat4_rotate_y(f32 angle);
pxl8_mat4 pxl8_mat4_rotate_z(f32 angle);
pxl8_mat4 pxl8_mat4_scale(f32 x, f32 y, f32 z);
pxl8_mat4 pxl8_mat4_translate(f32 x, f32 y, f32 z);
pxl8_frustum pxl8_frustum_from_matrix(pxl8_mat4 vp);
bool pxl8_frustum_test_aabb(const pxl8_frustum* frustum, pxl8_vec3 min, pxl8_vec3 max);
#ifdef __cplusplus
}
#endif

View file

@ -1,316 +0,0 @@
#include "pxl8_repl.h"
#include <poll.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <linenoise.h>
#define PXL8_MAX_REPL_COMMAND_SIZE 4096
#define PXL8_REPL_QUEUE_SIZE 8
struct pxl8_repl_command {
char buffer[PXL8_MAX_REPL_COMMAND_SIZE];
};
struct pxl8_repl {
char commands[PXL8_REPL_QUEUE_SIZE][PXL8_MAX_REPL_COMMAND_SIZE];
atomic_uint cmd_write_idx;
atomic_uint cmd_read_idx;
atomic_bool cmd_complete;
char logs[PXL8_REPL_QUEUE_SIZE][PXL8_MAX_REPL_COMMAND_SIZE];
atomic_uint log_write_idx;
atomic_uint log_read_idx;
atomic_bool should_quit;
pthread_t thread;
char accumulator[PXL8_MAX_REPL_COMMAND_SIZE];
pxl8_repl_command command;
};
static pxl8_repl* g_repl = NULL;
static void pxl8_repl_completion(const char* buf, linenoiseCompletions* lc) {
const char* fennel_keywords[] = {
"fn", "let", "var", "set", "global", "local",
"if", "when", "do", "while", "for", "each",
"lambda", "λ", "partial", "macro", "macros",
"require", "include", "import-macros",
"values", "select", "table", "length",
".", "..", ":", "->", "->>", "-?>", "-?>>",
"doto", "match", "case", "pick-values",
"collect", "icollect", "accumulate"
};
const char* pxl8_functions[] = {
"pxl8.clr", "pxl8.pixel", "pxl8.get_pixel",
"pxl8.line", "pxl8.rect", "pxl8.rect_fill",
"pxl8.circle", "pxl8.circle_fill", "pxl8.text",
"pxl8.get_screen", "pxl8.info", "pxl8.warn",
"pxl8.error", "pxl8.debug", "pxl8.trace"
};
size_t buf_len = strlen(buf);
for (size_t i = 0; i < sizeof(fennel_keywords) / sizeof(fennel_keywords[0]); i++) {
if (strncmp(buf, fennel_keywords[i], buf_len) == 0) {
linenoiseAddCompletion(lc, fennel_keywords[i]);
}
}
for (size_t i = 0; i < sizeof(pxl8_functions) / sizeof(pxl8_functions[0]); i++) {
if (strncmp(buf, pxl8_functions[i], buf_len) == 0) {
linenoiseAddCompletion(lc, pxl8_functions[i]);
}
}
}
static char* pxl8_repl_hints(const char* buf, int* color, int* bold) {
if (strncmp(buf, "pxl8.", 5) == 0 && strlen(buf) == 5) {
*color = 35;
*bold = 0;
return "clr|pixel|line|rect|circle|text|get_screen";
}
if (strcmp(buf, "(fn") == 0) {
*color = 36;
*bold = 0;
return " [args] body)";
}
if (strcmp(buf, "(let") == 0) {
*color = 36;
*bold = 0;
return " [bindings] body)";
}
return NULL;
}
static void pxl8_repl_flush_logs(pxl8_repl* repl) {
u32 log_read_idx = atomic_load(&repl->log_read_idx);
u32 log_write_idx = atomic_load(&repl->log_write_idx);
while (log_read_idx != log_write_idx) {
printf("%s", repl->logs[log_read_idx]);
atomic_store(&repl->log_read_idx, (log_read_idx + 1) % PXL8_REPL_QUEUE_SIZE);
log_read_idx = atomic_load(&repl->log_read_idx);
log_write_idx = atomic_load(&repl->log_write_idx);
}
fflush(stdout);
}
static void* pxl8_repl_thread(void* arg) {
pxl8_repl* repl = (pxl8_repl*)arg;
printf("[pxl8 REPL] Fennel 1.6.0 - Tab for completion, Ctrl-D to exit\n");
fflush(stdout);
struct linenoiseState ls;
char input_buf[PXL8_MAX_REPL_COMMAND_SIZE];
bool editing = false;
struct pollfd pfd = {
.fd = STDIN_FILENO,
.events = POLLIN
};
while (!atomic_load(&repl->should_quit)) {
u32 log_read_idx = atomic_load(&repl->log_read_idx);
u32 log_write_idx = atomic_load(&repl->log_write_idx);
if (log_read_idx != log_write_idx) {
printf("\r\033[K");
if (editing) {
linenoiseEditStop(&ls);
editing = false;
printf("\033[A\r\033[K");
}
while (log_read_idx != log_write_idx) {
printf("%s", repl->logs[log_read_idx]);
atomic_store(&repl->log_read_idx, (log_read_idx + 1) % PXL8_REPL_QUEUE_SIZE);
log_read_idx = atomic_load(&repl->log_read_idx);
log_write_idx = atomic_load(&repl->log_write_idx);
}
fflush(stdout);
continue;
}
if (!editing && !atomic_load(&repl->should_quit)) {
const char* prompt = (repl->accumulator[0] != '\0') ? ".. " : ">> ";
if (linenoiseEditStart(&ls, STDIN_FILENO, STDOUT_FILENO, input_buf, sizeof(input_buf), prompt) == -1) {
atomic_store(&repl->should_quit, true);
break;
}
editing = true;
}
if (poll(&pfd, 1, 1) <= 0) continue;
char* line = linenoiseEditFeed(&ls);
if (line == NULL) {
atomic_store(&repl->should_quit, true);
break;
}
if (line == linenoiseEditMore) continue;
linenoiseEditStop(&ls);
editing = false;
bool in_multiline = (repl->accumulator[0] != '\0');
if (strlen(line) > 0 || in_multiline) {
if (!in_multiline) {
linenoiseHistoryAdd(line);
linenoiseHistorySave(".pxl8_history");
}
if (repl->accumulator[0] != '\0') {
strncat(repl->accumulator, "\n",
PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1);
}
strncat(repl->accumulator, line,
PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1);
u32 write_idx = atomic_load(&repl->cmd_write_idx);
u32 read_idx = atomic_load(&repl->cmd_read_idx);
u32 next_write = (write_idx + 1) % PXL8_REPL_QUEUE_SIZE;
if (next_write != read_idx) {
strncpy(repl->commands[write_idx], repl->accumulator, PXL8_MAX_REPL_COMMAND_SIZE - 1);
repl->commands[write_idx][PXL8_MAX_REPL_COMMAND_SIZE - 1] = '\0';
atomic_store(&repl->cmd_write_idx, next_write);
}
}
linenoiseFree(line);
while (!atomic_load(&repl->should_quit) &&
(atomic_load(&repl->cmd_write_idx) != atomic_load(&repl->cmd_read_idx) ||
!atomic_load(&repl->cmd_complete))) {
u32 lr = atomic_load(&repl->log_read_idx);
u32 lw = atomic_load(&repl->log_write_idx);
while (lr != lw) {
printf("%s", repl->logs[lr]);
atomic_store(&repl->log_read_idx, (lr + 1) % PXL8_REPL_QUEUE_SIZE);
lr = atomic_load(&repl->log_read_idx);
lw = atomic_load(&repl->log_write_idx);
}
fflush(stdout);
struct timespec ts = {.tv_sec = 0, .tv_nsec = 1000000};
nanosleep(&ts, NULL);
}
atomic_store(&repl->cmd_complete, false);
}
if (editing) linenoiseEditStop(&ls);
pxl8_repl_flush_logs(repl);
return NULL;
}
pxl8_repl* pxl8_repl_create(void) {
pxl8_repl* repl = (pxl8_repl*)calloc(1, sizeof(pxl8_repl));
if (!repl) return NULL;
repl->accumulator[0] = '\0';
atomic_store(&repl->cmd_write_idx, 0);
atomic_store(&repl->cmd_read_idx, 0);
atomic_store(&repl->cmd_complete, true);
atomic_store(&repl->log_write_idx, 0);
atomic_store(&repl->log_read_idx, 0);
atomic_store(&repl->should_quit, false);
linenoiseHistorySetMaxLen(100);
linenoiseSetMultiLine(1);
linenoiseSetCompletionCallback(pxl8_repl_completion);
linenoiseSetHintsCallback(pxl8_repl_hints);
linenoiseHistoryLoad(".pxl8_history");
g_repl = repl;
if (pthread_create(&repl->thread, NULL, pxl8_repl_thread, repl) != 0) {
free(repl);
g_repl = NULL;
return NULL;
}
return repl;
}
void pxl8_repl_destroy(pxl8_repl* repl) {
if (!repl) return;
atomic_store(&repl->should_quit, true);
struct timespec ts = {.tv_sec = 0, .tv_nsec = 2000000};
nanosleep(&ts, NULL);
printf("\r\033[K");
fflush(stdout);
pthread_join(repl->thread, NULL);
pxl8_repl_flush_logs(repl);
g_repl = NULL;
system("stty sane 2>/dev/null");
free(repl);
}
pxl8_repl_command* pxl8_repl_pop_command(pxl8_repl* repl) {
if (!repl) return NULL;
u32 read_idx = atomic_load(&repl->cmd_read_idx);
u32 write_idx = atomic_load(&repl->cmd_write_idx);
if (read_idx == write_idx) return NULL;
strncpy(repl->command.buffer, repl->commands[read_idx], PXL8_MAX_REPL_COMMAND_SIZE - 1);
repl->command.buffer[PXL8_MAX_REPL_COMMAND_SIZE - 1] = '\0';
atomic_store(&repl->cmd_read_idx, (read_idx + 1) % PXL8_REPL_QUEUE_SIZE);
return &repl->command;
}
const char* pxl8_repl_command_buffer(pxl8_repl_command* cmd) {
return cmd ? cmd->buffer : NULL;
}
bool pxl8_repl_should_quit(pxl8_repl* repl) {
return repl ? atomic_load(&repl->should_quit) : false;
}
bool pxl8_repl_push_log(const char* message) {
if (!g_repl || !message) return false;
u32 write_idx = atomic_load(&g_repl->log_write_idx);
u32 read_idx = atomic_load(&g_repl->log_read_idx);
u32 next_write = (write_idx + 1) % PXL8_REPL_QUEUE_SIZE;
if (next_write != read_idx) {
strncpy(g_repl->logs[write_idx], message, PXL8_MAX_REPL_COMMAND_SIZE - 1);
g_repl->logs[write_idx][PXL8_MAX_REPL_COMMAND_SIZE - 1] = '\0';
atomic_store(&g_repl->log_write_idx, next_write);
}
return true;
}
void pxl8_repl_clear_accumulator(pxl8_repl* repl) {
if (!repl) return;
repl->accumulator[0] = '\0';
}
void pxl8_repl_signal_complete(pxl8_repl* repl) {
if (!repl) return;
atomic_store(&repl->cmd_complete, true);
}

View file

@ -1,24 +0,0 @@
#pragma once
#include "pxl8_types.h"
typedef struct pxl8_repl pxl8_repl;
typedef struct pxl8_repl_command pxl8_repl_command;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_repl* pxl8_repl_create(void);
void pxl8_repl_destroy(pxl8_repl* repl);
pxl8_repl_command* pxl8_repl_pop_command(pxl8_repl* repl);
const char* pxl8_repl_command_buffer(pxl8_repl_command* cmd);
bool pxl8_repl_push_log(const char* message);
void pxl8_repl_clear_accumulator(pxl8_repl* repl);
bool pxl8_repl_should_quit(pxl8_repl* repl);
void pxl8_repl_signal_complete(pxl8_repl* repl);
#ifdef __cplusplus
}
#endif

View file

@ -1,619 +0,0 @@
#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;
}

View file

@ -1,84 +0,0 @@
#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

View file

@ -1,24 +0,0 @@
#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);
}

View file

@ -1,20 +0,0 @@
#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

View file

@ -1,272 +0,0 @@
#include "pxl8_save.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <errno.h>
#ifdef _WIN32
#include <direct.h>
#include <shlobj.h>
#define PATH_SEP '\\'
#else
#include <unistd.h>
#include <pwd.h>
#define PATH_SEP '/'
#endif
#include "pxl8_log.h"
typedef struct {
u32 magic;
u32 version;
u32 size;
u32 checksum;
} pxl8_save_header;
struct pxl8_save {
char directory[PXL8_SAVE_MAX_PATH];
u32 magic;
u32 version;
};
static u32 pxl8_save_checksum(const u8* data, u32 size) {
u32 hash = 2166136261u;
for (u32 i = 0; i < size; i++) {
hash ^= data[i];
hash *= 16777619u;
}
return hash;
}
static void pxl8_save_get_slot_path(pxl8_save* save, u8 slot, char* path, size_t path_size) {
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
snprintf(path, path_size, "%s%chotreload.sav", save->directory, PATH_SEP);
} else {
snprintf(path, path_size, "%s%csave%d.sav", save->directory, PATH_SEP, slot);
}
}
static pxl8_result pxl8_save_ensure_directory(const char* path) {
struct stat st;
if (stat(path, &st) == 0) {
return S_ISDIR(st.st_mode) ? PXL8_OK : PXL8_ERROR_SYSTEM_FAILURE;
}
#ifdef _WIN32
if (_mkdir(path) != 0 && errno != EEXIST) {
#else
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
#endif
return PXL8_ERROR_SYSTEM_FAILURE;
}
return PXL8_OK;
}
pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version) {
if (!game_name) return NULL;
pxl8_save* save = (pxl8_save*)calloc(1, sizeof(pxl8_save));
if (!save) return NULL;
save->magic = magic;
save->version = version;
char base_dir[PXL8_SAVE_MAX_PATH];
#ifdef _WIN32
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, base_dir))) {
snprintf(save->directory, sizeof(save->directory),
"%s%cpxl8%c%s", base_dir, PATH_SEP, PATH_SEP, game_name);
} else {
free(save);
return NULL;
}
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
if (pw) home = pw->pw_dir;
}
if (!home) {
free(save);
return NULL;
}
snprintf(base_dir, sizeof(base_dir), "%s/.local/share", home);
pxl8_save_ensure_directory(base_dir);
snprintf(base_dir, sizeof(base_dir), "%s/.local/share/pxl8", home);
pxl8_save_ensure_directory(base_dir);
snprintf(save->directory, sizeof(save->directory),
"%s/.local/share/pxl8/%s", home, game_name);
#endif
if (pxl8_save_ensure_directory(save->directory) != PXL8_OK) {
free(save);
return NULL;
}
pxl8_info("Save system initialized: %s", save->directory);
return save;
}
void pxl8_save_destroy(pxl8_save* save) {
if (save) {
free(save);
}
}
pxl8_result pxl8_save_write(pxl8_save* save, u8 slot, const u8* data, u32 size) {
if (!save) return PXL8_ERROR_NULL_POINTER;
if (!data || size == 0) return PXL8_ERROR_NULL_POINTER;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
FILE* file = fopen(path, "wb");
if (!file) {
pxl8_error("Failed to open save file for writing: %s", path);
return PXL8_ERROR_SYSTEM_FAILURE;
}
pxl8_save_header header = {
.magic = save->magic,
.version = save->version,
.size = size,
.checksum = pxl8_save_checksum(data, size)
};
bool success = true;
if (fwrite(&header, sizeof(header), 1, file) != 1) success = false;
if (success && fwrite(data, 1, size, file) != size) success = false;
fclose(file);
if (!success) {
pxl8_error("Failed to write save data");
return PXL8_ERROR_SYSTEM_FAILURE;
}
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
pxl8_debug("Hot reload state saved (%u bytes)", size);
} else {
pxl8_info("Game saved to slot %d (%u bytes)", slot, size);
}
return PXL8_OK;
}
pxl8_result pxl8_save_read(pxl8_save* save, u8 slot, u8** data_out, u32* size_out) {
if (!save) return PXL8_ERROR_NULL_POINTER;
if (!data_out || !size_out) return PXL8_ERROR_NULL_POINTER;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
*data_out = NULL;
*size_out = 0;
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
FILE* file = fopen(path, "rb");
if (!file) {
return PXL8_ERROR_FILE_NOT_FOUND;
}
pxl8_save_header header;
if (fread(&header, sizeof(header), 1, file) != 1) {
fclose(file);
return PXL8_ERROR_INVALID_FORMAT;
}
if (header.magic != save->magic) {
fclose(file);
pxl8_error("Invalid save file magic");
return PXL8_ERROR_INVALID_FORMAT;
}
if (header.version > save->version) {
fclose(file);
pxl8_error("Save file version too new: %u", header.version);
return PXL8_ERROR_INVALID_FORMAT;
}
u8* data = (u8*)malloc(header.size);
if (!data) {
fclose(file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
if (fread(data, 1, header.size, file) != header.size) {
free(data);
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fclose(file);
u32 checksum = pxl8_save_checksum(data, header.size);
if (checksum != header.checksum) {
free(data);
pxl8_error("Save file checksum mismatch");
return PXL8_ERROR_INVALID_FORMAT;
}
*data_out = data;
*size_out = header.size;
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
pxl8_debug("Hot reload state loaded (%u bytes)", header.size);
} else {
pxl8_info("Game loaded from slot %d (%u bytes)", slot, header.size);
}
return PXL8_OK;
}
void pxl8_save_free(u8* data) {
if (data) {
free(data);
}
}
bool pxl8_save_exists(pxl8_save* save, u8 slot) {
if (!save) return false;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return false;
}
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
struct stat st;
return stat(path, &st) == 0;
}
pxl8_result pxl8_save_delete(pxl8_save* save, u8 slot) {
if (!save) return PXL8_ERROR_NULL_POINTER;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
if (remove(path) != 0 && errno != ENOENT) {
return PXL8_ERROR_SYSTEM_FAILURE;
}
return PXL8_OK;
}
const char* pxl8_save_get_directory(pxl8_save* save) {
return save ? save->directory : NULL;
}

View file

@ -1,27 +0,0 @@
#pragma once
#include "pxl8_types.h"
#define PXL8_SAVE_MAX_SLOTS 10
#define PXL8_SAVE_HOTRELOAD_SLOT 255
#define PXL8_SAVE_MAX_PATH 512
typedef struct pxl8_save pxl8_save;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version);
void pxl8_save_destroy(pxl8_save* save);
pxl8_result pxl8_save_delete(pxl8_save* save, u8 slot);
bool pxl8_save_exists(pxl8_save* save, u8 slot);
void pxl8_save_free(u8* data);
const char* pxl8_save_get_directory(pxl8_save* save);
pxl8_result pxl8_save_read(pxl8_save* save, u8 slot, u8** data_out, u32* size_out);
pxl8_result pxl8_save_write(pxl8_save* save, u8 slot, const u8* data, u32 size);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load diff

View file

@ -1,44 +0,0 @@
#pragma once
#include "pxl8_cart.h"
#include "pxl8_gfx.h"
#include "pxl8_sfx.h"
#include "pxl8_types.h"
typedef struct pxl8_script pxl8_script;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_script* pxl8_script_create(bool repl_mode);
void pxl8_script_destroy(pxl8_script* script);
const char* pxl8_script_get_last_error(pxl8_script* script);
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);
pxl8_result pxl8_script_call_function(pxl8_script* script, const char* name);
pxl8_result pxl8_script_call_function_f32(pxl8_script* script, const char* name, f32 arg);
bool pxl8_script_check_reload(pxl8_script* script);
pxl8_result pxl8_script_eval(pxl8_script* script, const char* code);
pxl8_result pxl8_script_eval_repl(pxl8_script* script, const char* code);
pxl8_result pxl8_script_load_cart_manifest(pxl8_script* script, pxl8_cart* cart);
pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path);
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

View file

@ -1,405 +0,0 @@
#include "pxl8_sdl3.h"
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "pxl8_color.h"
#include "pxl8_log.h"
#include "pxl8_sys.h"
typedef struct pxl8_sdl3_context {
SDL_Texture* framebuffer;
SDL_Renderer* renderer;
SDL_Window* window;
u32* rgba_buffer;
size_t rgba_buffer_size;
} pxl8_sdl3_context;
static void* sdl3_create(i32 render_w, i32 render_h,
const char* title, i32 win_w, i32 win_h) {
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)SDL_calloc(1, sizeof(pxl8_sdl3_context));
if (!ctx) {
pxl8_error("Failed to allocate SDL3 context");
return NULL;
}
ctx->window = SDL_CreateWindow(title, win_w, win_h, SDL_WINDOW_RESIZABLE);
if (!ctx->window) {
pxl8_error("Failed to create window: %s", SDL_GetError());
SDL_free(ctx);
return NULL;
}
ctx->renderer = SDL_CreateRenderer(ctx->window, NULL);
if (!ctx->renderer) {
pxl8_error("Failed to create renderer: %s", SDL_GetError());
SDL_DestroyWindow(ctx->window);
SDL_free(ctx);
return NULL;
}
if (!SDL_SetRenderVSync(ctx->renderer, 1)) {
pxl8_error("Failed to set vsync: %s", SDL_GetError());
}
ctx->framebuffer = SDL_CreateTexture(ctx->renderer,
SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
render_w, render_h);
if (!ctx->framebuffer) {
pxl8_error("Failed to create framebuffer texture: %s", SDL_GetError());
SDL_DestroyRenderer(ctx->renderer);
SDL_DestroyWindow(ctx->window);
SDL_free(ctx);
return NULL;
}
SDL_SetTextureScaleMode(ctx->framebuffer, SDL_SCALEMODE_NEAREST);
ctx->rgba_buffer = NULL;
ctx->rgba_buffer_size = 0;
return ctx;
}
static void sdl3_destroy(void* platform_data) {
if (!platform_data) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
if (ctx->rgba_buffer) SDL_free(ctx->rgba_buffer);
if (ctx->framebuffer) SDL_DestroyTexture(ctx->framebuffer);
if (ctx->renderer) SDL_DestroyRenderer(ctx->renderer);
if (ctx->window) SDL_DestroyWindow(ctx->window);
SDL_free(ctx);
}
static u64 sdl3_get_ticks(void) {
return SDL_GetTicksNS();
}
static void sdl3_present(void* platform_data) {
if (!platform_data) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
SDL_SetRenderDrawColor(ctx->renderer, 0, 0, 0, 255);
SDL_RenderClear(ctx->renderer);
if (ctx->framebuffer) {
SDL_RenderTexture(ctx->renderer, ctx->framebuffer, NULL, NULL);
}
SDL_RenderPresent(ctx->renderer);
}
static void sdl3_upload_texture(void* platform_data, const u8* pixels, u32 w, u32 h,
const u32* palette, u32 bpp) {
if (!platform_data || !pixels) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
size_t needed_size = w * h;
if (ctx->rgba_buffer_size < needed_size) {
u32* new_buffer = (u32*)SDL_realloc(ctx->rgba_buffer, needed_size * 4);
if (!new_buffer) return;
ctx->rgba_buffer = new_buffer;
ctx->rgba_buffer_size = needed_size;
}
if (bpp == 2) {
const u16* pixels16 = (const u16*)pixels;
for (u32 i = 0; i < w * h; i++) {
ctx->rgba_buffer[i] = pxl8_rgb565_to_rgba32(pixels16[i]);
}
} else {
for (u32 i = 0; i < w * h; i++) {
ctx->rgba_buffer[i] = palette[pixels[i]];
}
}
SDL_UpdateTexture(ctx->framebuffer, NULL, ctx->rgba_buffer, w * 4);
}
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_AUDIO)) {
pxl8_error("SDL_Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
pxl8* sys = pxl8_create(&pxl8_hal_sdl3);
if (!sys) {
pxl8_error("Failed to create pxl8 system");
SDL_Quit();
return SDL_APP_FAILURE;
}
pxl8_result result = pxl8_init(sys, argc, argv);
if (result != PXL8_OK) {
pxl8_destroy(sys);
SDL_Quit();
return SDL_APP_FAILURE;
}
*appstate = sys;
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void* appstate) {
pxl8* sys = (pxl8*)appstate;
if (!sys) {
return SDL_APP_FAILURE;
}
pxl8_result update_result = pxl8_update(sys);
if (update_result != PXL8_OK) {
return SDL_APP_FAILURE;
}
pxl8_result frame_result = pxl8_frame(sys);
if (frame_result != PXL8_OK) {
return SDL_APP_FAILURE;
}
return pxl8_is_running(sys) ? SDL_APP_CONTINUE : SDL_APP_SUCCESS;
}
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) {
pxl8* sys = (pxl8*)appstate;
if (!sys) {
return SDL_APP_CONTINUE;
}
pxl8_input_state* input = pxl8_get_input(sys);
if (!input) {
return SDL_APP_CONTINUE;
}
switch (event->type) {
case SDL_EVENT_QUIT:
pxl8_set_running(sys, false);
break;
case SDL_EVENT_KEY_DOWN: {
SDL_Scancode scancode = event->key.scancode;
if (scancode < PXL8_MAX_KEYS) {
if (!input->keys_down[scancode]) {
input->keys_pressed[scancode] = true;
}
input->keys_down[scancode] = true;
input->keys_released[scancode] = false;
}
break;
}
case SDL_EVENT_KEY_UP: {
SDL_Scancode scancode = event->key.scancode;
if (scancode < PXL8_MAX_KEYS) {
input->keys_down[scancode] = false;
input->keys_pressed[scancode] = false;
input->keys_released[scancode] = true;
}
break;
}
case SDL_EVENT_MOUSE_BUTTON_DOWN: {
u8 button = event->button.button - 1;
if (button < 3) {
if (!input->mouse_buttons_down[button]) {
input->mouse_buttons_pressed[button] = true;
}
input->mouse_buttons_down[button] = true;
input->mouse_buttons_released[button] = false;
}
break;
}
case SDL_EVENT_MOUSE_BUTTON_UP: {
u8 button = event->button.button - 1;
if (button < 3) {
input->mouse_buttons_down[button] = false;
input->mouse_buttons_pressed[button] = false;
input->mouse_buttons_released[button] = true;
}
break;
}
case SDL_EVENT_MOUSE_MOTION: {
pxl8_gfx* gfx = pxl8_get_gfx(sys);
if (!gfx) break;
input->mouse_dx = (i32)event->motion.xrel;
input->mouse_dy = (i32)event->motion.yrel;
i32 window_mouse_x = (i32)event->motion.x;
i32 window_mouse_y = (i32)event->motion.y;
SDL_Window* window = SDL_GetWindowFromID(event->motion.windowID);
if (!window) break;
i32 window_width, window_height;
SDL_GetWindowSize(window, &window_width, &window_height);
i32 render_w = pxl8_gfx_get_width(gfx);
i32 render_h = pxl8_gfx_get_height(gfx);
pxl8_bounds window_bounds = {0, 0, window_width, window_height};
pxl8_viewport vp = pxl8_gfx_viewport(window_bounds, render_w, render_h);
input->mouse_x = (i32)((window_mouse_x - vp.offset_x) / vp.scale);
input->mouse_y = (i32)((window_mouse_y - vp.offset_y) / vp.scale);
break;
}
case SDL_EVENT_MOUSE_WHEEL: {
input->mouse_wheel_x = (i32)event->wheel.x;
input->mouse_wheel_y = (i32)event->wheel.y;
break;
}
}
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void* appstate, SDL_AppResult result) {
(void)result;
pxl8* sys = (pxl8*)appstate;
if (sys) {
pxl8_quit(sys);
pxl8_destroy(sys);
}
SDL_Quit();
}
static void sdl3_set_relative_mouse_mode(void* platform_data, bool enabled) {
if (!platform_data) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
if (!SDL_SetWindowRelativeMouseMode(ctx->window, enabled)) {
pxl8_error("Failed to set relative mouse mode: %s", SDL_GetError());
}
}
static void sdl3_set_cursor(void* platform_data, u32 cursor) {
if (!platform_data) return;
SDL_SystemCursor sdl_cursor;
switch (cursor) {
case PXL8_CURSOR_ARROW:
sdl_cursor = SDL_SYSTEM_CURSOR_DEFAULT;
break;
case PXL8_CURSOR_HAND:
sdl_cursor = SDL_SYSTEM_CURSOR_POINTER;
break;
default:
sdl_cursor = SDL_SYSTEM_CURSOR_DEFAULT;
break;
}
SDL_Cursor* cursor_obj = SDL_CreateSystemCursor(sdl_cursor);
if (cursor_obj) {
SDL_SetCursor(cursor_obj);
}
}
static void sdl3_center_cursor(void* platform_data) {
if (!platform_data) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
i32 w, h;
if (SDL_GetWindowSize(ctx->window, &w, &h)) {
SDL_WarpMouseInWindow(ctx->window, w / 2, h / 2);
}
}
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,
.get_ticks = sdl3_get_ticks,
.center_cursor = sdl3_center_cursor,
.present = sdl3_present,
.set_cursor = sdl3_set_cursor,
.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,
};

View file

@ -1,5 +0,0 @@
#pragma once
#include "pxl8_hal.h"
extern const pxl8_hal pxl8_hal_sdl3;

File diff suppressed because it is too large Load diff

View file

@ -1,141 +0,0 @@
#pragma once
#include "pxl8_hal.h"
#include "pxl8_types.h"
#define PXL8_SFX_BUFFER_SIZE 1024
#define PXL8_SFX_MAX_CONTEXTS 8
#define PXL8_SFX_MAX_DELAY_SAMPLES 48000
#define PXL8_SFX_MAX_VOICES 16
#define PXL8_SFX_SAMPLE_RATE 48000
typedef struct pxl8_sfx_context pxl8_sfx_context;
typedef struct pxl8_sfx_mixer pxl8_sfx_mixer;
typedef struct pxl8_sfx_node pxl8_sfx_node;
typedef enum pxl8_sfx_filter_type {
PXL8_SFX_FILTER_BANDPASS = 0,
PXL8_SFX_FILTER_HIGHPASS,
PXL8_SFX_FILTER_LOWPASS,
PXL8_SFX_FILTER_NONE
} pxl8_sfx_filter_type;
typedef enum pxl8_sfx_lfo_target {
PXL8_SFX_LFO_AMPLITUDE = 0,
PXL8_SFX_LFO_FILTER,
PXL8_SFX_LFO_PITCH
} pxl8_sfx_lfo_target;
typedef enum pxl8_sfx_node_type {
PXL8_SFX_NODE_COMPRESSOR,
PXL8_SFX_NODE_DELAY,
PXL8_SFX_NODE_REVERB
} pxl8_sfx_node_type;
typedef enum pxl8_sfx_waveform {
PXL8_SFX_WAVE_NOISE = 0,
PXL8_SFX_WAVE_PULSE,
PXL8_SFX_WAVE_SAW,
PXL8_SFX_WAVE_SINE,
PXL8_SFX_WAVE_SQUARE,
PXL8_SFX_WAVE_TRIANGLE
} pxl8_sfx_waveform;
typedef struct pxl8_sfx_adsr {
f32 attack;
f32 decay;
f32 sustain;
f32 release;
} pxl8_sfx_adsr;
typedef struct pxl8_sfx_compressor_config {
f32 attack;
f32 ratio;
f32 release;
f32 threshold;
} pxl8_sfx_compressor_config;
typedef struct pxl8_sfx_delay_config {
f32 feedback;
f32 mix;
u32 time_l;
u32 time_r;
} pxl8_sfx_delay_config;
typedef struct pxl8_sfx_reverb_config {
f32 damping;
f32 mix;
f32 room;
} pxl8_sfx_reverb_config;
typedef struct pxl8_sfx_voice_params {
pxl8_sfx_adsr amp_env;
pxl8_sfx_adsr filter_env;
pxl8_sfx_filter_type filter_type;
pxl8_sfx_lfo_target lfo_target;
pxl8_sfx_waveform lfo_waveform;
pxl8_sfx_waveform waveform;
f32 filter_cutoff;
f32 filter_env_depth;
f32 filter_resonance;
f32 fx_send;
f32 lfo_depth;
f32 lfo_rate;
f32 pulse_width;
} pxl8_sfx_voice_params;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_sfx_node* pxl8_sfx_compressor_create(pxl8_sfx_compressor_config cfg);
void pxl8_sfx_compressor_set_attack(pxl8_sfx_node* node, f32 attack);
void pxl8_sfx_compressor_set_ratio(pxl8_sfx_node* node, f32 ratio);
void pxl8_sfx_compressor_set_release(pxl8_sfx_node* node, f32 release);
void pxl8_sfx_compressor_set_threshold(pxl8_sfx_node* node, f32 threshold);
void pxl8_sfx_context_append_node(pxl8_sfx_context* ctx, pxl8_sfx_node* node);
pxl8_sfx_context* pxl8_sfx_context_create(void);
void pxl8_sfx_context_destroy(pxl8_sfx_context* ctx);
pxl8_sfx_node* pxl8_sfx_context_get_head(pxl8_sfx_context* ctx);
f32 pxl8_sfx_context_get_volume(const pxl8_sfx_context* ctx);
void pxl8_sfx_context_insert_node(pxl8_sfx_context* ctx, pxl8_sfx_node* after, pxl8_sfx_node* node);
void pxl8_sfx_context_remove_node(pxl8_sfx_context* ctx, pxl8_sfx_node* node);
void pxl8_sfx_context_set_volume(pxl8_sfx_context* ctx, f32 volume);
pxl8_sfx_node* pxl8_sfx_delay_create(pxl8_sfx_delay_config cfg);
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(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, 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);
void pxl8_sfx_reverb_set_damping(pxl8_sfx_node* node, f32 damping);
void pxl8_sfx_reverb_set_mix(pxl8_sfx_node* node, f32 mix);
void pxl8_sfx_reverb_set_room(pxl8_sfx_node* node, f32 room);
void pxl8_sfx_stop_all(pxl8_sfx_context* ctx);
void pxl8_sfx_stop_voice(pxl8_sfx_context* ctx, u16 voice_id);
#ifdef __cplusplus
}
#endif

View file

@ -1,37 +0,0 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_hal.h"
#include "pxl8_io.h"
#include "pxl8_sfx.h"
#include "pxl8_types.h"
typedef struct pxl8 pxl8;
#ifdef __cplusplus
extern "C" {
#endif
pxl8* pxl8_create(const pxl8_hal* hal);
void pxl8_destroy(pxl8* sys);
pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]);
pxl8_result pxl8_update(pxl8* sys);
pxl8_result pxl8_frame(pxl8* sys);
void pxl8_quit(pxl8* sys);
f32 pxl8_get_fps(const pxl8* sys);
pxl8_gfx* pxl8_get_gfx(const pxl8* sys);
pxl8_input_state* pxl8_get_input(const pxl8* sys);
pxl8_size pxl8_get_resolution_dimensions(pxl8_resolution resolution);
pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys);
bool pxl8_is_running(const pxl8* sys);
void pxl8_center_cursor(pxl8* sys);
void pxl8_set_cursor(pxl8* sys, pxl8_cursor cursor);
void pxl8_set_relative_mouse_mode(pxl8* sys, bool enabled);
void pxl8_set_running(pxl8* sys, bool running);
#ifdef __cplusplus
}
#endif

View file

@ -1,614 +0,0 @@
#include "pxl8_tilemap.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_ase.h"
#include "pxl8_log.h"
#include "pxl8_macros.h"
#include "pxl8_tilesheet.h"
struct pxl8_tilesheet {
u8* data;
bool* tile_valid;
u32 height;
u32 tile_size;
u32 tiles_per_row;
u32 total_tiles;
u32 width;
pxl8_pixel_mode pixel_mode;
u32 ref_count;
pxl8_tile_animation* animations;
u32 animation_count;
pxl8_tile_properties* properties;
};
struct pxl8_tilemap_layer {
u32 allocated_chunks;
u32 chunk_count;
pxl8_tile_chunk** chunks;
u32 chunks_high;
u32 chunks_wide;
u8 opacity;
bool visible;
};
struct pxl8_tilemap {
u32 active_layers;
i32 camera_x;
i32 camera_y;
u32 height;
pxl8_tilemap_layer layers[PXL8_MAX_TILE_LAYERS];
u32 tile_size;
pxl8_tilesheet* tilesheet;
u32 tile_user_data_capacity;
void** tile_user_data;
u32 width;
};
static inline u32 pxl8_chunk_index(u32 x, u32 y, u32 chunks_wide) {
return (y >> 4) * chunks_wide + (x >> 4);
}
static pxl8_tile_chunk* pxl8_get_or_create_chunk(pxl8_tilemap_layer* layer, u32 x, u32 y) {
u32 chunk_x = x >> 4;
u32 chunk_y = y >> 4;
u32 idx = chunk_y * layer->chunks_wide + chunk_x;
if (idx >= layer->chunks_wide * layer->chunks_high) return NULL;
if (!layer->chunks[idx]) {
layer->chunks[idx] = calloc(1, sizeof(pxl8_tile_chunk));
if (!layer->chunks[idx]) return NULL;
layer->chunks[idx]->chunk_x = chunk_x;
layer->chunks[idx]->chunk_y = chunk_y;
layer->chunks[idx]->empty = true;
layer->allocated_chunks++;
}
return layer->chunks[idx];
}
pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size) {
if (width > PXL8_MAX_TILEMAP_WIDTH || height > PXL8_MAX_TILEMAP_HEIGHT) {
return NULL;
}
pxl8_tilemap* tilemap = calloc(1, sizeof(pxl8_tilemap));
if (!tilemap) return NULL;
tilemap->width = width;
tilemap->height = height;
tilemap->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE;
tilemap->active_layers = 1;
tilemap->tilesheet = pxl8_tilesheet_create(tilemap->tile_size);
if (!tilemap->tilesheet) {
free(tilemap);
return NULL;
}
tilemap->tile_user_data = NULL;
tilemap->tile_user_data_capacity = 0;
u32 chunks_wide = (width + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
u32 chunks_high = (height + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
pxl8_tilemap_layer* layer = &tilemap->layers[i];
layer->chunks_wide = chunks_wide;
layer->chunks_high = chunks_high;
layer->chunk_count = chunks_wide * chunks_high;
layer->visible = (i == 0);
layer->opacity = 255;
layer->chunks = calloc(layer->chunk_count, sizeof(pxl8_tile_chunk*));
if (!layer->chunks) {
for (u32 j = 0; j < i; j++) {
free(tilemap->layers[j].chunks);
}
if (tilemap->tilesheet) pxl8_tilesheet_destroy(tilemap->tilesheet);
free(tilemap);
return NULL;
}
}
return tilemap;
}
void pxl8_tilemap_destroy(pxl8_tilemap* tilemap) {
if (!tilemap) return;
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
pxl8_tilemap_layer* layer = &tilemap->layers[i];
if (layer->chunks) {
for (u32 j = 0; j < layer->chunk_count; j++) {
if (layer->chunks[j]) {
free(layer->chunks[j]);
}
}
free(layer->chunks);
}
}
if (tilemap->tilesheet) pxl8_tilesheet_unref(tilemap->tilesheet);
if (tilemap->tile_user_data) free(tilemap->tile_user_data);
free(tilemap);
}
u32 pxl8_tilemap_get_width(const pxl8_tilemap* tilemap) {
return tilemap ? tilemap->width : 0;
}
u32 pxl8_tilemap_get_height(const pxl8_tilemap* tilemap) {
return tilemap ? tilemap->height : 0;
}
u32 pxl8_tilemap_get_tile_size(const pxl8_tilemap* tilemap) {
return tilemap ? tilemap->tile_size : 0;
}
void pxl8_tilemap_set_tile_user_data(pxl8_tilemap* tilemap, u16 tile_id, void* user_data) {
if (!tilemap || tile_id == 0) return;
if (tile_id >= tilemap->tile_user_data_capacity) {
u32 new_capacity = tile_id + 64;
void** new_data = realloc(tilemap->tile_user_data, new_capacity * sizeof(void*));
if (!new_data) return;
for (u32 i = tilemap->tile_user_data_capacity; i < new_capacity; i++) {
new_data[i] = NULL;
}
tilemap->tile_user_data = new_data;
tilemap->tile_user_data_capacity = new_capacity;
}
tilemap->tile_user_data[tile_id] = user_data;
}
void* pxl8_tilemap_get_tile_user_data(const pxl8_tilemap* tilemap, u16 tile_id) {
if (!tilemap || tile_id == 0 || tile_id >= tilemap->tile_user_data_capacity) return NULL;
return tilemap->tile_user_data[tile_id];
}
pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet) {
if (!tilemap || !tilesheet) return PXL8_ERROR_NULL_POINTER;
if (tilemap->tilesheet) {
pxl8_tilesheet_unref(tilemap->tilesheet);
}
tilemap->tilesheet = tilesheet;
pxl8_tilesheet_ref(tilesheet);
u32 tilesheet_size = pxl8_tilesheet_get_tile_size(tilesheet);
if (tilesheet_size != tilemap->tile_size) {
pxl8_warn("Tilesheet tile size (%d) differs from tilemap tile size (%d)",
tilesheet_size, tilemap->tile_size);
tilemap->tile_size = tilesheet_size;
}
return PXL8_OK;
}
pxl8_result pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags) {
if (!tilemap) return PXL8_ERROR_NULL_POINTER;
if (layer >= PXL8_MAX_TILE_LAYERS) return PXL8_ERROR_INVALID_ARGUMENT;
if (x >= tilemap->width || y >= tilemap->height) return PXL8_ERROR_INVALID_COORDINATE;
pxl8_tilemap_layer* l = &tilemap->layers[layer];
pxl8_tile_chunk* chunk = pxl8_get_or_create_chunk(l, x, y);
if (!chunk) return PXL8_ERROR_OUT_OF_MEMORY;
u32 local_x = x & PXL8_CHUNK_MASK;
u32 local_y = y & PXL8_CHUNK_MASK;
u32 idx = local_y * PXL8_CHUNK_SIZE + local_x;
chunk->tiles[idx] = pxl8_tile_pack(tile_id, flags, 0);
if (tile_id != 0) {
chunk->empty = false;
}
if (layer >= tilemap->active_layers) {
tilemap->active_layers = layer + 1;
l->visible = true;
}
return PXL8_OK;
}
pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y) {
if (!tilemap || layer >= PXL8_MAX_TILE_LAYERS) return 0;
if (x >= tilemap->width || y >= tilemap->height) return 0;
const pxl8_tilemap_layer* l = &tilemap->layers[layer];
u32 chunk_idx = pxl8_chunk_index(x, y, l->chunks_wide);
if (chunk_idx >= l->chunk_count || !l->chunks[chunk_idx]) return 0;
const pxl8_tile_chunk* chunk = l->chunks[chunk_idx];
u32 local_x = x & PXL8_CHUNK_MASK;
u32 local_y = y & PXL8_CHUNK_MASK;
u32 idx = local_y * PXL8_CHUNK_SIZE + local_x;
return chunk->tiles[idx];
}
void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y) {
if (!tilemap) return;
tilemap->camera_x = x;
tilemap->camera_y = y;
}
void pxl8_tilemap_get_view(const pxl8_tilemap* tilemap, const pxl8_gfx* gfx, pxl8_tilemap_view* view) {
if (!tilemap || !gfx || !view) return;
view->x = -tilemap->camera_x;
view->y = -tilemap->camera_y;
view->width = pxl8_gfx_get_width(gfx);
view->height = pxl8_gfx_get_height(gfx);
view->tile_start_x = pxl8_max(0, tilemap->camera_x / (i32)tilemap->tile_size);
view->tile_start_y = pxl8_max(0, tilemap->camera_y / (i32)tilemap->tile_size);
view->tile_end_x = pxl8_min((i32)tilemap->width,
(tilemap->camera_x + view->width) / (i32)tilemap->tile_size + 1);
view->tile_end_y = pxl8_min((i32)tilemap->height,
(tilemap->camera_y + view->height) / (i32)tilemap->tile_size + 1);
}
void pxl8_tilemap_render_tile(const pxl8_tilemap* tilemap, pxl8_gfx* gfx, u16 tile_id, i32 x, i32 y, u8 flags) {
if (!tilemap || !gfx || !tilemap->tilesheet) return;
pxl8_tilesheet_render_tile(tilemap->tilesheet, gfx, tile_id, x, y, flags);
}
void pxl8_tilemap_render_layer(const pxl8_tilemap* tilemap, pxl8_gfx* gfx, u32 layer) {
if (!tilemap || !gfx || layer >= tilemap->active_layers) return;
if (!tilemap->tilesheet) {
pxl8_warn("No tilesheet set for tilemap");
return;
}
const pxl8_tilemap_layer* l = &tilemap->layers[layer];
if (!l->visible) return;
i32 view_left = tilemap->camera_x / tilemap->tile_size;
i32 view_top = tilemap->camera_y / tilemap->tile_size;
i32 view_right = (tilemap->camera_x + pxl8_gfx_get_width(gfx)) / tilemap->tile_size + 1;
i32 view_bottom = (tilemap->camera_y + pxl8_gfx_get_height(gfx)) / tilemap->tile_size + 1;
u32 chunk_left = pxl8_max(0, view_left >> 4);
u32 chunk_top = pxl8_max(0, view_top >> 4);
u32 chunk_right = pxl8_min((tilemap->width + 15) >> 4, (u32)((view_right >> 4) + 1));
u32 chunk_bottom = pxl8_min((tilemap->height + 15) >> 4, (u32)((view_bottom >> 4) + 1));
for (u32 cy = chunk_top; cy < chunk_bottom; cy++) {
for (u32 cx = chunk_left; cx < chunk_right; cx++) {
u32 chunk_idx = cy * l->chunks_wide + cx;
if (chunk_idx >= l->chunk_count || !l->chunks[chunk_idx]) continue;
const pxl8_tile_chunk* chunk = l->chunks[chunk_idx];
if (chunk->empty) continue;
u32 tile_start_x = pxl8_max(0, view_left - (i32)(cx << 4));
u32 tile_end_x = pxl8_min(PXL8_CHUNK_SIZE, view_right - (i32)(cx << 4));
u32 tile_start_y = pxl8_max(0, view_top - (i32)(cy << 4));
u32 tile_end_y = pxl8_min(PXL8_CHUNK_SIZE, view_bottom - (i32)(cy << 4));
for (u32 ty = tile_start_y; ty < tile_end_y; ty++) {
for (u32 tx = tile_start_x; tx < tile_end_x; tx++) {
u32 idx = ty * PXL8_CHUNK_SIZE + tx;
pxl8_tile tile = chunk->tiles[idx];
u16 tile_id = pxl8_tile_get_id(tile);
if (tile_id == 0) continue;
i32 world_x = (cx << 4) + tx;
i32 world_y = (cy << 4) + ty;
i32 screen_x = world_x * tilemap->tile_size - tilemap->camera_x;
i32 screen_y = world_y * tilemap->tile_size - tilemap->camera_y;
u8 flags = pxl8_tile_get_flags(tile);
if (flags & PXL8_TILE_ANIMATED && tilemap->tilesheet) {
tile_id = pxl8_tilesheet_get_animated_frame(tilemap->tilesheet, tile_id);
}
pxl8_tilesheet_render_tile(tilemap->tilesheet, gfx, tile_id, screen_x, screen_y, flags);
}
}
}
}
}
void pxl8_tilemap_render(const pxl8_tilemap* tilemap, pxl8_gfx* gfx) {
if (!tilemap || !gfx) return;
for (u32 layer = 0; layer < tilemap->active_layers; layer++) {
pxl8_tilemap_render_layer(tilemap, gfx, layer);
}
}
bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y) {
if (!tilemap) return true;
u32 tile_x = x / tilemap->tile_size;
u32 tile_y = y / tilemap->tile_size;
for (u32 layer = 0; layer < tilemap->active_layers; layer++) {
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tile_x, tile_y);
if (pxl8_tile_get_flags(tile) & PXL8_TILE_SOLID) return true;
}
return false;
}
bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h) {
if (!tilemap) return true;
i32 left = x / tilemap->tile_size;
i32 top = y / tilemap->tile_size;
i32 right = (x + w - 1) / tilemap->tile_size;
i32 bottom = (y + h - 1) / tilemap->tile_size;
for (i32 ty = top; ty <= bottom; ty++) {
for (i32 tx = left; tx <= right; tx++) {
if (tx < 0 || tx >= (i32)tilemap->width ||
ty < 0 || ty >= (i32)tilemap->height) return true;
for (u32 layer = 0; layer < tilemap->active_layers; layer++) {
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty);
if (pxl8_tile_get_flags(tile) & PXL8_TILE_SOLID) return true;
}
}
}
return false;
}
u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y) {
if (!tilemap) return 0;
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, x, y);
return pxl8_tile_get_id(tile);
}
void pxl8_tilemap_update(pxl8_tilemap* tilemap, f32 delta_time) {
if (!tilemap || !tilemap->tilesheet) return;
pxl8_tilesheet_update_animations(tilemap->tilesheet, delta_time);
}
static u8 pxl8_tilemap_get_neighbors(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 match_id) {
u8 neighbors = 0;
if (y > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x, y - 1) == match_id)
neighbors |= 1 << 0;
if (x < tilemap->width - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x + 1, y) == match_id)
neighbors |= 1 << 1;
if (y < tilemap->height - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x, y + 1) == match_id)
neighbors |= 1 << 2;
if (x > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x - 1, y) == match_id)
neighbors |= 1 << 3;
if (y > 0 && x < tilemap->width - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x + 1, y - 1) == match_id)
neighbors |= 1 << 4;
if (y < tilemap->height - 1 && x < tilemap->width - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x + 1, y + 1) == match_id)
neighbors |= 1 << 5;
if (y < tilemap->height - 1 && x > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x - 1, y + 1) == match_id)
neighbors |= 1 << 6;
if (y > 0 && x > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x - 1, y - 1) == match_id)
neighbors |= 1 << 7;
return neighbors;
}
pxl8_result pxl8_tilemap_set_tile_auto(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y,
u16 base_tile_id, u8 flags) {
if (!tilemap || !tilemap->tilesheet) return PXL8_ERROR_NULL_POINTER;
pxl8_result result = pxl8_tilemap_set_tile(tilemap, layer, x, y, base_tile_id, flags | PXL8_TILE_AUTOTILE);
if (result != PXL8_OK) return result;
pxl8_tilemap_update_autotiles(tilemap, layer,
x > 0 ? x - 1 : x, y > 0 ? y - 1 : y,
x < tilemap->width - 1 ? 3 : 2,
y < tilemap->height - 1 ? 3 : 2);
return PXL8_OK;
}
void pxl8_tilemap_update_autotiles(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u32 w, u32 h) {
if (!tilemap || !tilemap->tilesheet) return;
for (u32 ty = y; ty < y + h && ty < tilemap->height; ty++) {
for (u32 tx = x; tx < x + w && tx < tilemap->width; tx++) {
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty);
u8 flags = pxl8_tile_get_flags(tile);
if (flags & PXL8_TILE_AUTOTILE) {
u16 base_id = pxl8_tile_get_id(tile);
u8 neighbors = pxl8_tilemap_get_neighbors(tilemap, layer, tx, ty, base_id);
u16 new_id = pxl8_tilesheet_apply_autotile(tilemap->tilesheet, base_id, neighbors);
if (new_id != base_id) {
pxl8_tilemap_layer* l = &tilemap->layers[layer];
pxl8_tile_chunk* chunk = pxl8_get_or_create_chunk(l, tx, ty);
if (chunk) {
u32 local_x = tx & PXL8_CHUNK_MASK;
u32 local_y = ty & PXL8_CHUNK_MASK;
u32 idx = local_y * PXL8_CHUNK_SIZE + local_x;
chunk->tiles[idx] = pxl8_tile_pack(new_id, flags, pxl8_tile_get_palette(tile));
}
}
}
}
}
}
u32 pxl8_tilemap_get_memory_usage(const pxl8_tilemap* tilemap) {
if (!tilemap) return 0;
u32 total = sizeof(pxl8_tilemap);
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
const pxl8_tilemap_layer* layer = &tilemap->layers[i];
total += layer->chunk_count * sizeof(pxl8_tile_chunk*);
total += layer->allocated_chunks * sizeof(pxl8_tile_chunk);
}
return total;
}
void pxl8_tilemap_compress(pxl8_tilemap* tilemap) {
if (!tilemap) return;
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
pxl8_tilemap_layer* layer = &tilemap->layers[i];
for (u32 j = 0; j < layer->chunk_count; j++) {
pxl8_tile_chunk* chunk = layer->chunks[j];
if (!chunk) continue;
bool has_tiles = false;
for (u32 k = 0; k < PXL8_CHUNK_SIZE * PXL8_CHUNK_SIZE; k++) {
if (pxl8_tile_get_id(chunk->tiles[k]) != 0) {
has_tiles = true;
break;
}
}
if (!has_tiles) {
free(chunk);
layer->chunks[j] = NULL;
layer->allocated_chunks--;
} else {
chunk->empty = false;
}
}
}
}
pxl8_result pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u32 layer) {
if (!tilemap || !filepath) return PXL8_ERROR_NULL_POINTER;
if (!tilemap->tilesheet) return PXL8_ERROR_NULL_POINTER;
if (layer >= PXL8_MAX_TILE_LAYERS) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_ase_file ase_file = {0};
pxl8_result result = pxl8_ase_load(filepath, &ase_file);
if (result != PXL8_OK) {
pxl8_error("Failed to load ASE file: %s", filepath);
return result;
}
if (ase_file.tileset_count == 0) {
pxl8_error("ASE file has no tileset - must be created as Tilemap in Aseprite: %s", filepath);
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_ase_tileset* tileset = &ase_file.tilesets[0];
if (tileset->tile_width != tilemap->tile_size || tileset->tile_height != tilemap->tile_size) {
pxl8_error("Tileset tile size (%ux%u) doesn't match tilemap tile size (%u)",
tileset->tile_width, tileset->tile_height, tilemap->tile_size);
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_info("Loading tilemap from %s: %u tiles, %ux%u each",
filepath, tileset->tile_count, tileset->tile_width, tileset->tile_height);
if (!(tileset->flags & 2)) {
pxl8_error("Tileset has no embedded tiles - external tilesets not yet supported");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
if (!tileset->pixels) {
pxl8_error("Tileset has no pixel data");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
u32 tiles_per_row = 16;
u32 tilesheet_rows = (tileset->tile_count + tiles_per_row - 1) / tiles_per_row;
u32 tilesheet_width = tiles_per_row * tilemap->tile_size;
u32 tilesheet_height = tilesheet_rows * tilemap->tile_size;
if (tilemap->tilesheet->data) free(tilemap->tilesheet->data);
tilemap->tilesheet->data = calloc(tilesheet_width * tilesheet_height, 1);
if (!tilemap->tilesheet->data) {
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
tilemap->tilesheet->width = tilesheet_width;
tilemap->tilesheet->height = tilesheet_height;
tilemap->tilesheet->tiles_per_row = tiles_per_row;
tilemap->tilesheet->total_tiles = tileset->tile_count;
tilemap->tilesheet->pixel_mode = PXL8_PIXEL_INDEXED;
if (tilemap->tilesheet->tile_valid) free(tilemap->tilesheet->tile_valid);
tilemap->tilesheet->tile_valid = calloc(tileset->tile_count + 1, sizeof(bool));
for (u32 i = 0; i < tileset->tile_count; i++) {
u32 sheet_row = i / tiles_per_row;
u32 sheet_col = i % tiles_per_row;
u32 src_offset = i * tilemap->tile_size * tilemap->tile_size;
for (u32 y = 0; y < tilemap->tile_size; y++) {
for (u32 x = 0; x < tilemap->tile_size; x++) {
u32 dst_x = sheet_col * tilemap->tile_size + x;
u32 dst_y = sheet_row * tilemap->tile_size + y;
u32 dst_idx = dst_y * tilesheet_width + dst_x;
u32 src_idx = src_offset + y * tilemap->tile_size + x;
if (src_idx < tileset->pixels_size && dst_idx < tilesheet_width * tilesheet_height) {
tilemap->tilesheet->data[dst_idx] = tileset->pixels[src_idx];
}
}
}
tilemap->tilesheet->tile_valid[i] = true;
}
pxl8_info("Loaded %u tiles into tilesheet (%ux%u)", tileset->tile_count, tilesheet_width, tilesheet_height);
if (ase_file.frame_count == 0 || !ase_file.frames) {
pxl8_error("ASE file has no frames");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_ase_frame* frame = &ase_file.frames[0];
pxl8_ase_cel* tilemap_cel = NULL;
for (u32 i = 0; i < frame->cel_count; i++) {
if (frame->cels[i].cel_type == 3) {
tilemap_cel = &frame->cels[i];
break;
}
}
if (!tilemap_cel || !tilemap_cel->tilemap.tiles) {
pxl8_error("No tilemap cel found in frame 0");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_info("Found tilemap cel: %ux%u tiles", tilemap_cel->tilemap.width, tilemap_cel->tilemap.height);
for (u32 ty = 0; ty < tilemap_cel->tilemap.height && ty < tilemap->height; ty++) {
for (u32 tx = 0; tx < tilemap_cel->tilemap.width && tx < tilemap->width; tx++) {
u32 tile_idx = ty * tilemap_cel->tilemap.width + tx;
u32 tile_value = tilemap_cel->tilemap.tiles[tile_idx];
u16 tile_id = (u16)(tile_value & tilemap_cel->tilemap.tile_id_mask);
u8 flags = 0;
if (tile_value & tilemap_cel->tilemap.x_flip_mask) flags |= PXL8_TILE_FLIP_X;
if (tile_value & tilemap_cel->tilemap.y_flip_mask) flags |= PXL8_TILE_FLIP_Y;
pxl8_tilemap_set_tile(tilemap, layer, tx, ty, tile_id, flags);
}
}
pxl8_ase_destroy(&ase_file);
return PXL8_OK;
}

View file

@ -1,123 +0,0 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_tilesheet.h"
#include "pxl8_types.h"
#define PXL8_TILE_SIZE 8
#define PXL8_MAX_TILEMAP_WIDTH 256
#define PXL8_MAX_TILEMAP_HEIGHT 256
#define PXL8_MAX_TILE_LAYERS 4
#define PXL8_CHUNK_SIZE 16
#define PXL8_CHUNK_MASK 15
typedef enum pxl8_tile_flags {
PXL8_TILE_FLIP_X = 1 << 0,
PXL8_TILE_FLIP_Y = 1 << 1,
PXL8_TILE_SOLID = 1 << 2,
PXL8_TILE_TRIGGER = 1 << 3,
PXL8_TILE_ANIMATED = 1 << 4,
PXL8_TILE_AUTOTILE = 1 << 5,
} pxl8_tile_flags;
#define PXL8_TILE_ID_MASK 0x0000FFFF
#define PXL8_TILE_FLAGS_MASK 0x00FF0000
#define PXL8_TILE_PAL_MASK 0xFF000000
#define PXL8_TILE_ID_SHIFT 0
#define PXL8_TILE_FLAGS_SHIFT 16
#define PXL8_TILE_PAL_SHIFT 24
typedef u32 pxl8_tile;
static inline pxl8_tile pxl8_tile_pack(u16 id, u8 flags, u8 palette_offset) {
return (u32)id | ((u32)flags << 16) | ((u32)palette_offset << 24);
}
static inline u16 pxl8_tile_get_id(pxl8_tile tile) {
return tile & PXL8_TILE_ID_MASK;
}
static inline u8 pxl8_tile_get_flags(pxl8_tile tile) {
return (tile & PXL8_TILE_FLAGS_MASK) >> PXL8_TILE_FLAGS_SHIFT;
}
static inline u8 pxl8_tile_get_palette(pxl8_tile tile) {
return (tile & PXL8_TILE_PAL_MASK) >> PXL8_TILE_PAL_SHIFT;
}
typedef struct pxl8_tile_animation {
u16 current_frame;
f32 frame_duration;
u16 frame_count;
u16* frames;
f32 time_accumulator;
} pxl8_tile_animation;
typedef struct pxl8_tile_properties {
i16 collision_offset_x;
i16 collision_offset_y;
u16 collision_height;
u16 collision_width;
u32 property_flags;
void* user_data;
} pxl8_tile_properties;
typedef struct pxl8_autotile_rule {
u8 neighbor_mask;
u16 tile_id;
} pxl8_autotile_rule;
typedef struct pxl8_tile_chunk {
u32 chunk_x;
u32 chunk_y;
bool empty;
pxl8_tile tiles[PXL8_CHUNK_SIZE * PXL8_CHUNK_SIZE];
} pxl8_tile_chunk;
typedef struct pxl8_tilemap_layer pxl8_tilemap_layer;
typedef struct pxl8_tilemap pxl8_tilemap;
typedef struct pxl8_tilemap_view {
i32 height;
i32 tile_end_x, tile_end_y;
i32 tile_start_x, tile_start_y;
i32 width;
i32 x, y;
} pxl8_tilemap_view;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size);
void pxl8_tilemap_destroy(pxl8_tilemap* tilemap);
u32 pxl8_tilemap_get_height(const pxl8_tilemap* tilemap);
u32 pxl8_tilemap_get_memory_usage(const pxl8_tilemap* tilemap);
pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);
u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);
u32 pxl8_tilemap_get_tile_size(const pxl8_tilemap* tilemap);
void* pxl8_tilemap_get_tile_user_data(const pxl8_tilemap* tilemap, u16 tile_id);
void pxl8_tilemap_get_view(const pxl8_tilemap* tilemap, const pxl8_gfx* gfx, pxl8_tilemap_view* view);
u32 pxl8_tilemap_get_width(const pxl8_tilemap* tilemap);
void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y);
pxl8_result pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags);
pxl8_result pxl8_tilemap_set_tile_auto(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 base_tile_id, u8 flags);
void pxl8_tilemap_set_tile_user_data(pxl8_tilemap* tilemap, u16 tile_id, void* user_data);
pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet);
pxl8_result pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u32 layer);
bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h);
void pxl8_tilemap_compress(pxl8_tilemap* tilemap);
bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y);
void pxl8_tilemap_render(const pxl8_tilemap* tilemap, pxl8_gfx* gfx);
void pxl8_tilemap_render_layer(const pxl8_tilemap* tilemap, pxl8_gfx* gfx, u32 layer);
void pxl8_tilemap_render_tile(const pxl8_tilemap* tilemap, pxl8_gfx* gfx, u16 tile_id, i32 x, i32 y, u8 flags);
void pxl8_tilemap_update(pxl8_tilemap* tilemap, f32 delta_time);
void pxl8_tilemap_update_autotiles(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u32 w, u32 h);
#ifdef __cplusplus
}
#endif

View file

@ -1,404 +0,0 @@
#include "pxl8_tilesheet.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_ase.h"
#include "pxl8_color.h"
#include "pxl8_log.h"
#include "pxl8_tilemap.h"
struct pxl8_tilesheet {
u8* data;
bool* tile_valid;
u32 height;
u32 tile_size;
u32 tiles_per_row;
u32 total_tiles;
u32 width;
pxl8_pixel_mode pixel_mode;
u32 ref_count;
pxl8_tile_animation* animations;
u32 animation_count;
pxl8_tile_properties* properties;
pxl8_autotile_rule** autotile_rules;
u32* autotile_rule_counts;
};
pxl8_tilesheet* pxl8_tilesheet_create(u32 tile_size) {
pxl8_tilesheet* tilesheet = calloc(1, sizeof(pxl8_tilesheet));
if (!tilesheet) return NULL;
tilesheet->tile_size = tile_size;
tilesheet->ref_count = 1;
return tilesheet;
}
void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet) {
if (!tilesheet) return;
if (tilesheet->data) {
free(tilesheet->data);
}
if (tilesheet->tile_valid) {
free(tilesheet->tile_valid);
}
if (tilesheet->animations) {
for (u32 i = 0; i < tilesheet->animation_count; i++) {
if (tilesheet->animations[i].frames) {
free(tilesheet->animations[i].frames);
}
}
free(tilesheet->animations);
}
if (tilesheet->properties) {
free(tilesheet->properties);
}
if (tilesheet->autotile_rules) {
for (u32 i = 0; i <= tilesheet->total_tiles; i++) {
if (tilesheet->autotile_rules[i]) {
free(tilesheet->autotile_rules[i]);
}
}
free(tilesheet->autotile_rules);
free(tilesheet->autotile_rule_counts);
}
free(tilesheet);
}
pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx* gfx) {
if (!tilesheet || !filepath || !gfx) return PXL8_ERROR_NULL_POINTER;
pxl8_ase_file ase_file;
pxl8_result result = pxl8_ase_load(filepath, &ase_file);
if (result != PXL8_OK) {
pxl8_error("Failed to load tilesheet: %s", filepath);
return result;
}
if (tilesheet->data) {
free(tilesheet->data);
}
u32 width = ase_file.header.width;
u32 height = ase_file.header.height;
if (ase_file.header.grid_width > 0 && ase_file.header.grid_height > 0) {
tilesheet->tile_size = ase_file.header.grid_width;
pxl8_info("Using Aseprite grid size: %dx%d",
ase_file.header.grid_width, ase_file.header.grid_height);
}
tilesheet->width = width;
tilesheet->height = height;
tilesheet->tiles_per_row = width / tilesheet->tile_size;
tilesheet->total_tiles = (width / tilesheet->tile_size) * (height / tilesheet->tile_size);
tilesheet->pixel_mode = pxl8_gfx_get_pixel_mode(gfx);
u32 pixel_count = width * height;
u16 ase_depth = ase_file.header.color_depth;
bool gfx_hicolor = (tilesheet->pixel_mode == PXL8_PIXEL_HICOLOR);
size_t data_size = pixel_count * pxl8_bytes_per_pixel(tilesheet->pixel_mode);
tilesheet->data = malloc(data_size);
if (!tilesheet->data) {
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
if (ase_file.frame_count > 0 && ase_file.frames[0].pixels) {
const u8* src = ase_file.frames[0].pixels;
if (ase_depth == 8 && !gfx_hicolor) {
memcpy(tilesheet->data, src, pixel_count);
} else if (ase_depth == 32 && gfx_hicolor) {
u16* dst = (u16*)tilesheet->data;
const u32* rgba = (const u32*)src;
for (u32 i = 0; i < pixel_count; i++) {
u32 c = rgba[i];
u8 a = (c >> 24) & 0xFF;
if (a == 0) {
dst[i] = 0;
} else {
dst[i] = pxl8_rgba32_to_rgb565(c);
}
}
} else if (ase_depth == 8 && gfx_hicolor) {
pxl8_warn("Indexed ASE with hicolor gfx - storing as indexed");
tilesheet->pixel_mode = PXL8_PIXEL_INDEXED;
u8* new_data = realloc(tilesheet->data, pixel_count);
if (!new_data) {
free(tilesheet->data);
tilesheet->data = NULL;
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
tilesheet->data = new_data;
memcpy(tilesheet->data, src, pixel_count);
} else {
pxl8_error("Unsupported ASE color depth %d for gfx mode", ase_depth);
free(tilesheet->data);
tilesheet->data = NULL;
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
}
tilesheet->tile_valid = calloc(tilesheet->total_tiles + 1, sizeof(bool));
if (!tilesheet->tile_valid) {
free(tilesheet->data);
tilesheet->data = NULL;
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
u32 valid_tiles = 0;
bool is_hicolor = (tilesheet->pixel_mode == PXL8_PIXEL_HICOLOR);
for (u32 tile_id = 1; tile_id <= tilesheet->total_tiles; tile_id++) {
u32 tile_x = ((tile_id - 1) % tilesheet->tiles_per_row) * tilesheet->tile_size;
u32 tile_y = ((tile_id - 1) / tilesheet->tiles_per_row) * tilesheet->tile_size;
bool has_content = false;
for (u32 py = 0; py < tilesheet->tile_size && !has_content; py++) {
for (u32 px = 0; px < tilesheet->tile_size; px++) {
u32 idx = (tile_y + py) * width + (tile_x + px);
if (is_hicolor) {
if (((u16*)tilesheet->data)[idx] != 0) {
has_content = true;
break;
}
} else {
if (tilesheet->data[idx] != 0) {
has_content = true;
break;
}
}
}
}
if (has_content) {
tilesheet->tile_valid[tile_id] = true;
valid_tiles++;
}
}
pxl8_info("Loaded tilesheet %s: %dx%d, %d valid tiles out of %d slots (%dx%d each)",
filepath, width, height, valid_tiles, tilesheet->total_tiles,
tilesheet->tile_size, tilesheet->tile_size);
pxl8_ase_destroy(&ase_file);
return PXL8_OK;
}
void pxl8_tilesheet_render_tile(const pxl8_tilesheet* tilesheet, pxl8_gfx* gfx,
u16 tile_id, i32 x, i32 y, u8 flags) {
if (!tilesheet || !gfx || !tilesheet->data) return;
if (tile_id == 0 || tile_id > tilesheet->total_tiles) return;
if (tilesheet->tile_valid && !tilesheet->tile_valid[tile_id]) return;
u32 tile_x = ((tile_id - 1) % tilesheet->tiles_per_row) * tilesheet->tile_size;
u32 tile_y = ((tile_id - 1) / tilesheet->tiles_per_row) * tilesheet->tile_size;
for (u32 py = 0; py < tilesheet->tile_size; py++) {
for (u32 px = 0; px < tilesheet->tile_size; px++) {
u32 src_x = tile_x + px;
u32 src_y = tile_y + py;
if (flags & PXL8_TILE_FLIP_X) src_x = tile_x + (tilesheet->tile_size - 1 - px);
if (flags & PXL8_TILE_FLIP_Y) src_y = tile_y + (tilesheet->tile_size - 1 - py);
u32 src_idx = src_y * tilesheet->width + src_x;
u8 color_idx = tilesheet->data[src_idx];
if (color_idx != 0) {
i32 screen_x = x + px;
i32 screen_y = y + py;
if (screen_x >= 0 && screen_x < pxl8_gfx_get_width(gfx) &&
screen_y >= 0 && screen_y < pxl8_gfx_get_height(gfx)) {
pxl8_pixel(gfx, screen_x, screen_y, color_idx);
}
}
}
}
}
bool pxl8_tilesheet_is_tile_valid(const pxl8_tilesheet* tilesheet, u16 tile_id) {
if (!tilesheet || tile_id == 0 || tile_id > tilesheet->total_tiles) return false;
return tilesheet->tile_valid ? tilesheet->tile_valid[tile_id] : true;
}
void pxl8_tilesheet_ref(pxl8_tilesheet* tilesheet) {
if (!tilesheet) return;
tilesheet->ref_count++;
}
void pxl8_tilesheet_unref(pxl8_tilesheet* tilesheet) {
if (!tilesheet) return;
if (--tilesheet->ref_count == 0) {
pxl8_tilesheet_destroy(tilesheet);
}
}
pxl8_result pxl8_tilesheet_add_animation(pxl8_tilesheet* tilesheet, u16 base_tile_id,
const u16* frames, u16 frame_count, f32 frame_duration) {
if (!tilesheet || !frames || frame_count == 0) return PXL8_ERROR_INVALID_ARGUMENT;
if (base_tile_id == 0 || base_tile_id > tilesheet->total_tiles) return PXL8_ERROR_INVALID_ARGUMENT;
if (!tilesheet->animations) {
tilesheet->animations = calloc(tilesheet->total_tiles + 1, sizeof(pxl8_tile_animation));
if (!tilesheet->animations) return PXL8_ERROR_OUT_OF_MEMORY;
}
pxl8_tile_animation* anim = &tilesheet->animations[base_tile_id];
if (anim->frames) free(anim->frames);
anim->frames = malloc(frame_count * sizeof(u16));
if (!anim->frames) return PXL8_ERROR_OUT_OF_MEMORY;
memcpy(anim->frames, frames, frame_count * sizeof(u16));
anim->frame_count = frame_count;
anim->current_frame = 0;
anim->frame_duration = frame_duration;
anim->time_accumulator = 0;
tilesheet->animation_count++;
return PXL8_OK;
}
void pxl8_tilesheet_update_animations(pxl8_tilesheet* tilesheet, f32 delta_time) {
if (!tilesheet || !tilesheet->animations) return;
for (u32 i = 1; i <= tilesheet->total_tiles; i++) {
pxl8_tile_animation* anim = &tilesheet->animations[i];
if (!anim->frames || anim->frame_count == 0) continue;
anim->time_accumulator += delta_time;
while (anim->time_accumulator >= anim->frame_duration) {
anim->time_accumulator -= anim->frame_duration;
anim->current_frame = (anim->current_frame + 1) % anim->frame_count;
}
}
}
u16 pxl8_tilesheet_get_animated_frame(const pxl8_tilesheet* tilesheet, u16 tile_id) {
if (!tilesheet || !tilesheet->animations || tile_id == 0 || tile_id > tilesheet->total_tiles) {
return tile_id;
}
const pxl8_tile_animation* anim = &tilesheet->animations[tile_id];
if (!anim->frames || anim->frame_count == 0) return tile_id;
return anim->frames[anim->current_frame];
}
pxl8_result pxl8_tilesheet_set_tile_pixels(pxl8_tilesheet* tilesheet, u16 tile_id, const u8* pixels) {
if (!tilesheet || !pixels || tile_id == 0 || tile_id > tilesheet->total_tiles) return PXL8_ERROR_INVALID_ARGUMENT;
if (!tilesheet->data) return PXL8_ERROR_NULL_POINTER;
u32 tile_x = (tile_id - 1) % tilesheet->tiles_per_row;
u32 tile_y = (tile_id - 1) / tilesheet->tiles_per_row;
u32 bytes_per_pixel = pxl8_bytes_per_pixel(tilesheet->pixel_mode);
for (u32 py = 0; py < tilesheet->tile_size; py++) {
for (u32 px = 0; px < tilesheet->tile_size; px++) {
u32 src_idx = py * tilesheet->tile_size + px;
u32 dst_x = tile_x * tilesheet->tile_size + px;
u32 dst_y = tile_y * tilesheet->tile_size + py;
u32 dst_idx = dst_y * tilesheet->width + dst_x;
if (bytes_per_pixel == 2) {
((u16*)tilesheet->data)[dst_idx] = ((const u16*)pixels)[src_idx];
} else {
tilesheet->data[dst_idx] = pixels[src_idx];
}
}
}
if (tilesheet->tile_valid) {
tilesheet->tile_valid[tile_id] = true;
}
return PXL8_OK;
}
void pxl8_tilesheet_set_tile_property(pxl8_tilesheet* tilesheet, u16 tile_id,
const pxl8_tile_properties* props) {
if (!tilesheet || !props || tile_id == 0 || tile_id > tilesheet->total_tiles) return;
if (!tilesheet->properties) {
tilesheet->properties = calloc(tilesheet->total_tiles + 1, sizeof(pxl8_tile_properties));
if (!tilesheet->properties) return;
}
tilesheet->properties[tile_id] = *props;
}
const pxl8_tile_properties* pxl8_tilesheet_get_tile_property(const pxl8_tilesheet* tilesheet, u16 tile_id) {
if (!tilesheet || !tilesheet->properties || tile_id == 0 || tile_id > tilesheet->total_tiles) {
return NULL;
}
return &tilesheet->properties[tile_id];
}
u32 pxl8_tilesheet_get_tile_size(const pxl8_tilesheet* tilesheet) {
return tilesheet ? tilesheet->tile_size : 0;
}
pxl8_result pxl8_tilesheet_add_autotile_rule(pxl8_tilesheet* tilesheet, u16 base_tile_id,
u8 neighbor_mask, u16 result_tile_id) {
if (!tilesheet || base_tile_id == 0 || base_tile_id > tilesheet->total_tiles) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
if (!tilesheet->autotile_rules) {
tilesheet->autotile_rules = calloc(tilesheet->total_tiles + 1, sizeof(pxl8_autotile_rule*));
tilesheet->autotile_rule_counts = calloc(tilesheet->total_tiles + 1, sizeof(u32));
if (!tilesheet->autotile_rules || !tilesheet->autotile_rule_counts) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
}
u32 count = tilesheet->autotile_rule_counts[base_tile_id];
pxl8_autotile_rule* new_rules = realloc(tilesheet->autotile_rules[base_tile_id],
(count + 1) * sizeof(pxl8_autotile_rule));
if (!new_rules) return PXL8_ERROR_OUT_OF_MEMORY;
new_rules[count].neighbor_mask = neighbor_mask;
new_rules[count].tile_id = result_tile_id;
tilesheet->autotile_rules[base_tile_id] = new_rules;
tilesheet->autotile_rule_counts[base_tile_id]++;
return PXL8_OK;
}
u16 pxl8_tilesheet_apply_autotile(const pxl8_tilesheet* tilesheet, u16 base_tile_id, u8 neighbors) {
if (!tilesheet || !tilesheet->autotile_rules || base_tile_id == 0 ||
base_tile_id > tilesheet->total_tiles) {
return base_tile_id;
}
pxl8_autotile_rule* rules = tilesheet->autotile_rules[base_tile_id];
u32 rule_count = tilesheet->autotile_rule_counts[base_tile_id];
for (u32 i = 0; i < rule_count; i++) {
if (rules[i].neighbor_mask == neighbors) {
return rules[i].tile_id;
}
}
return base_tile_id;
}

View file

@ -1,38 +0,0 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_types.h"
typedef struct pxl8_autotile_rule pxl8_autotile_rule;
typedef struct pxl8_tile_animation pxl8_tile_animation;
typedef struct pxl8_tile_properties pxl8_tile_properties;
typedef struct pxl8_tilesheet pxl8_tilesheet;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_tilesheet* pxl8_tilesheet_create(u32 tile_size);
void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet);
u16 pxl8_tilesheet_get_animated_frame(const pxl8_tilesheet* tilesheet, u16 tile_id);
const pxl8_tile_properties* pxl8_tilesheet_get_tile_property(const pxl8_tilesheet* tilesheet, u16 tile_id);
u32 pxl8_tilesheet_get_tile_size(const pxl8_tilesheet* tilesheet);
bool pxl8_tilesheet_is_tile_valid(const pxl8_tilesheet* tilesheet, u16 tile_id);
pxl8_result pxl8_tilesheet_set_tile_pixels(pxl8_tilesheet* tilesheet, u16 tile_id, const u8* pixels);
void pxl8_tilesheet_set_tile_property(pxl8_tilesheet* tilesheet, u16 tile_id, const pxl8_tile_properties* props);
pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx* gfx);
void pxl8_tilesheet_ref(pxl8_tilesheet* tilesheet);
void pxl8_tilesheet_unref(pxl8_tilesheet* tilesheet);
pxl8_result pxl8_tilesheet_add_animation(pxl8_tilesheet* tilesheet, u16 base_tile_id, const u16* frames, u16 frame_count, f32 frame_duration);
pxl8_result pxl8_tilesheet_add_autotile_rule(pxl8_tilesheet* tilesheet, u16 base_tile_id, u8 neighbor_mask, u16 result_tile_id);
u16 pxl8_tilesheet_apply_autotile(const pxl8_tilesheet* tilesheet, u16 base_tile_id, u8 neighbors);
void pxl8_tilesheet_render_tile(const pxl8_tilesheet* tilesheet, pxl8_gfx* gfx, u16 tile_id, i32 x, i32 y, u8 flags);
void pxl8_tilesheet_update_animations(pxl8_tilesheet* tilesheet, f32 delta_time);
#ifdef __cplusplus
}
#endif

View file

@ -1,251 +0,0 @@
#include "pxl8_transition.h"
#include <stdlib.h>
#include <math.h>
#include "pxl8_log.h"
#include "pxl8_math.h"
pxl8_transition* pxl8_transition_create(pxl8_transition_type type, f32 duration) {
if (duration <= 0.0f) {
pxl8_error("Invalid transition duration: %f", duration);
return NULL;
}
pxl8_transition* transition = (pxl8_transition*)calloc(1, sizeof(pxl8_transition));
if (!transition) {
pxl8_error("Failed to allocate transition");
return NULL;
}
transition->type = type;
transition->duration = duration;
transition->time = 0.0f;
transition->active = false;
transition->reverse = false;
transition->color = 0xFF000000;
transition->on_complete = NULL;
transition->userdata = NULL;
return transition;
}
void pxl8_transition_destroy(pxl8_transition* transition) {
if (!transition) return;
free(transition);
}
f32 pxl8_transition_get_progress(const pxl8_transition* transition) {
if (!transition) return 0.0f;
f32 t = transition->time / transition->duration;
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
return transition->reverse ? 1.0f - t : t;
}
bool pxl8_transition_is_active(const pxl8_transition* transition) {
return transition && transition->active;
}
bool pxl8_transition_is_complete(const pxl8_transition* transition) {
if (!transition) return true;
return transition->time >= transition->duration;
}
void pxl8_transition_render(const pxl8_transition* transition, pxl8_gfx* gfx) {
if (!transition || !gfx || !transition->active) return;
f32 progress = pxl8_transition_get_progress(transition);
i32 width = pxl8_gfx_get_width(gfx);
i32 height = pxl8_gfx_get_height(gfx);
switch (transition->type) {
case PXL8_TRANSITION_FADE: {
u8 alpha = (u8)(progress * 255.0f);
u32 fade_color = (transition->color & 0x00FFFFFF) | (alpha << 24);
for (i32 y = 0; y < height; y++) {
for (i32 x = 0; x < width; x++) {
u32 bg = pxl8_get_pixel(gfx, x, y);
u32 r_bg = (bg >> 16) & 0xFF;
u32 g_bg = (bg >> 8) & 0xFF;
u32 b_bg = bg & 0xFF;
u32 r_fg = (fade_color >> 16) & 0xFF;
u32 g_fg = (fade_color >> 8) & 0xFF;
u32 b_fg = fade_color & 0xFF;
u32 r = (r_bg * (255 - alpha) + r_fg * alpha) / 255;
u32 g = (g_bg * (255 - alpha) + g_fg * alpha) / 255;
u32 b = (b_bg * (255 - alpha) + b_fg * alpha) / 255;
pxl8_pixel(gfx, x, y, 0xFF000000 | (r << 16) | (g << 8) | b);
}
}
break;
}
case PXL8_TRANSITION_WIPE_LEFT: {
i32 wipe_x = (i32)(width * progress);
pxl8_rect_fill(gfx, 0, 0, wipe_x, height, transition->color);
break;
}
case PXL8_TRANSITION_WIPE_RIGHT: {
i32 wipe_x = (i32)(width * (1.0f - progress));
pxl8_rect_fill(gfx, wipe_x, 0, width - wipe_x, height, transition->color);
break;
}
case PXL8_TRANSITION_WIPE_UP: {
i32 wipe_y = (i32)(height * progress);
pxl8_rect_fill(gfx, 0, 0, width, wipe_y, transition->color);
break;
}
case PXL8_TRANSITION_WIPE_DOWN: {
i32 wipe_y = (i32)(height * (1.0f - progress));
pxl8_rect_fill(gfx, 0, wipe_y, width, height - wipe_y, transition->color);
break;
}
case PXL8_TRANSITION_CIRCLE_CLOSE: {
i32 center_x = width / 2;
i32 center_y = height / 2;
i32 max_radius = (i32)sqrtf((f32)(center_x * center_x + center_y * center_y));
i32 radius = (i32)(max_radius * (1.0f - progress));
for (i32 y = 0; y < height; y++) {
for (i32 x = 0; x < width; x++) {
i32 dx = x - center_x;
i32 dy = y - center_y;
i32 dist = (i32)sqrtf((f32)(dx * dx + dy * dy));
if (dist > radius) {
pxl8_pixel(gfx, x, y, transition->color);
}
}
}
break;
}
case PXL8_TRANSITION_CIRCLE_OPEN: {
i32 center_x = width / 2;
i32 center_y = height / 2;
i32 max_radius = (i32)sqrtf((f32)(center_x * center_x + center_y * center_y));
i32 radius = (i32)(max_radius * progress);
for (i32 y = 0; y < height; y++) {
for (i32 x = 0; x < width; x++) {
i32 dx = x - center_x;
i32 dy = y - center_y;
i32 dist = (i32)sqrtf((f32)(dx * dx + dy * dy));
if (dist < radius) {
pxl8_pixel(gfx, x, y, transition->color);
}
}
}
break;
}
case PXL8_TRANSITION_DISSOLVE: {
u32 seed = 12345;
for (i32 y = 0; y < height; y++) {
for (i32 x = 0; x < width; x++) {
seed = seed * 1103515245 + 12345;
f32 noise = (f32)((seed / 65536) % 1000) / 1000.0f;
if (noise < progress) {
pxl8_pixel(gfx, x, y, transition->color);
}
}
}
break;
}
case PXL8_TRANSITION_PIXELATE: {
i32 max_block_size = 32;
i32 block_size = (i32)(max_block_size * progress);
if (block_size < 1) block_size = 1;
pxl8_pixel_mode mode = pxl8_gfx_get_pixel_mode(gfx);
bool has_fb = (mode == PXL8_PIXEL_HICOLOR)
? (pxl8_gfx_get_framebuffer_hicolor(gfx) != NULL)
: (pxl8_gfx_get_framebuffer_indexed(gfx) != NULL);
if (!has_fb) break;
for (i32 y = 0; y < height; y += block_size) {
for (i32 x = 0; x < width; x += block_size) {
u32 color_sum_r = 0, color_sum_g = 0, color_sum_b = 0;
i32 count = 0;
for (i32 by = 0; by < block_size && y + by < height; by++) {
for (i32 bx = 0; bx < block_size && x + bx < width; bx++) {
u32 color = pxl8_get_pixel(gfx, x + bx, y + by);
color_sum_r += (color >> 16) & 0xFF;
color_sum_g += (color >> 8) & 0xFF;
color_sum_b += color & 0xFF;
count++;
}
}
if (count > 0) {
u32 avg_color = 0xFF000000 |
((color_sum_r / count) << 16) |
((color_sum_g / count) << 8) |
(color_sum_b / count);
for (i32 by = 0; by < block_size && y + by < height; by++) {
for (i32 bx = 0; bx < block_size && x + bx < width; bx++) {
pxl8_pixel(gfx, x + bx, y + by, avg_color);
}
}
}
}
}
break;
}
}
}
void pxl8_transition_reset(pxl8_transition* transition) {
if (!transition) return;
transition->time = 0.0f;
transition->active = false;
}
void pxl8_transition_set_color(pxl8_transition* transition, u32 color) {
if (!transition) return;
transition->color = color;
}
void pxl8_transition_set_reverse(pxl8_transition* transition, bool reverse) {
if (!transition) return;
transition->reverse = reverse;
}
void pxl8_transition_start(pxl8_transition* transition) {
if (!transition) return;
transition->active = true;
transition->time = 0.0f;
}
void pxl8_transition_stop(pxl8_transition* transition) {
if (!transition) return;
transition->active = false;
}
void pxl8_transition_update(pxl8_transition* transition, f32 dt) {
if (!transition || !transition->active) return;
transition->time += dt;
if (transition->time >= transition->duration) {
transition->time = transition->duration;
transition->active = false;
if (transition->on_complete) {
transition->on_complete(transition->userdata);
}
}
}

View file

@ -1,53 +0,0 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_types.h"
typedef enum pxl8_transition_type {
PXL8_TRANSITION_FADE,
PXL8_TRANSITION_WIPE_LEFT,
PXL8_TRANSITION_WIPE_RIGHT,
PXL8_TRANSITION_WIPE_UP,
PXL8_TRANSITION_WIPE_DOWN,
PXL8_TRANSITION_CIRCLE_OPEN,
PXL8_TRANSITION_CIRCLE_CLOSE,
PXL8_TRANSITION_DISSOLVE,
PXL8_TRANSITION_PIXELATE
} pxl8_transition_type;
typedef struct pxl8_transition {
pxl8_transition_type type;
f32 duration;
f32 time;
bool active;
bool reverse;
u32 color;
void (*on_complete)(void* userdata);
void* userdata;
} pxl8_transition;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_transition* pxl8_transition_create(pxl8_transition_type type, f32 duration);
void pxl8_transition_destroy(pxl8_transition* transition);
f32 pxl8_transition_get_progress(const pxl8_transition* transition);
bool pxl8_transition_is_active(const pxl8_transition* transition);
bool pxl8_transition_is_complete(const pxl8_transition* transition);
void pxl8_transition_set_color(pxl8_transition* transition, u32 color);
void pxl8_transition_set_reverse(pxl8_transition* transition, bool reverse);
void pxl8_transition_render(const pxl8_transition* transition, pxl8_gfx* gfx);
void pxl8_transition_reset(pxl8_transition* transition);
void pxl8_transition_start(pxl8_transition* transition);
void pxl8_transition_stop(pxl8_transition* transition);
void pxl8_transition_update(pxl8_transition* transition, f32 dt);
#ifdef __cplusplus
}
#endif

View file

@ -1,105 +0,0 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define PXL8_DEFAULT_ATLAS_ENTRY_CAPACITY 64
#define PXL8_DEFAULT_ATLAS_SIZE 1024
#define PXL8_DEFAULT_SPRITE_CACHE_CAPACITY 64
#define PXL8_MAX_ERROR_SIZE 2048
#define PXL8_MAX_KEYS 256
#define PXL8_MAX_PALETTE_SIZE 256
#define PXL8_MAX_PATH 256
typedef float f32;
typedef double f64;
typedef int8_t i8;
typedef int16_t i16;
typedef int32_t i32;
typedef int64_t i64;
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
#if defined(__SIZEOF_INT128__)
typedef __int128_t i128;
typedef __uint128_t u128;
#endif
typedef enum pxl8_pixel_mode {
PXL8_PIXEL_INDEXED,
PXL8_PIXEL_HICOLOR
} pxl8_pixel_mode;
typedef enum pxl8_cursor {
PXL8_CURSOR_ARROW,
PXL8_CURSOR_HAND
} pxl8_cursor;
typedef enum pxl8_resolution {
PXL8_RESOLUTION_240x160,
PXL8_RESOLUTION_320x180,
PXL8_RESOLUTION_320x240,
PXL8_RESOLUTION_640x360,
PXL8_RESOLUTION_640x480,
PXL8_RESOLUTION_800x600,
PXL8_RESOLUTION_960x540
} pxl8_resolution;
typedef enum pxl8_result {
PXL8_OK = 0,
PXL8_ERROR_ASE_INVALID_FRAME_MAGIC,
PXL8_ERROR_ASE_INVALID_MAGIC,
PXL8_ERROR_ASE_MALFORMED_CHUNK,
PXL8_ERROR_ASE_TRUNCATED_FILE,
PXL8_ERROR_FILE_NOT_FOUND,
PXL8_ERROR_INITIALIZATION_FAILED,
PXL8_ERROR_INVALID_ARGUMENT,
PXL8_ERROR_INVALID_COORDINATE,
PXL8_ERROR_INVALID_FORMAT,
PXL8_ERROR_INVALID_SIZE,
PXL8_ERROR_NOT_INITIALIZED,
PXL8_ERROR_NULL_POINTER,
PXL8_ERROR_OUT_OF_MEMORY,
PXL8_ERROR_SCRIPT_ERROR,
PXL8_ERROR_SYSTEM_FAILURE
} pxl8_result;
typedef struct pxl8_bounds {
i32 x, y;
i32 w;
i32 h;
} pxl8_bounds;
typedef struct pxl8_input_state {
bool keys_down[PXL8_MAX_KEYS];
bool keys_pressed[PXL8_MAX_KEYS];
bool keys_released[PXL8_MAX_KEYS];
bool mouse_buttons_down[3];
bool mouse_buttons_pressed[3];
bool mouse_buttons_released[3];
i32 mouse_wheel_x;
i32 mouse_wheel_y;
i32 mouse_x;
i32 mouse_y;
i32 mouse_dx;
i32 mouse_dy;
bool mouse_relative_mode;
} pxl8_input_state;
typedef struct pxl8_point {
i32 x, y;
} pxl8_point;
typedef struct pxl8_size {
i32 w, h;
} pxl8_size;
typedef struct pxl8_viewport {
i32 offset_x, offset_y;
i32 scaled_width, scaled_height;
f32 scale;
} pxl8_viewport;

View file

@ -1,504 +0,0 @@
#include "pxl8_vfx.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_math.h"
struct pxl8_particles {
pxl8_particle* particles;
pxl8_rng* rng;
u32 alive_count;
u32 count;
u32 max_count;
f32 x, y;
f32 spread_x, spread_y;
f32 drag;
f32 gravity_x, gravity_y;
f32 turbulence;
f32 spawn_rate;
f32 spawn_timer;
void (*render_fn)(pxl8_gfx* gfx, 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;
};
void pxl8_vfx_plasma(pxl8_gfx* gfx, f32 time, f32 scale1, f32 scale2, u8 palette_offset) {
if (!gfx || !pxl8_gfx_get_framebuffer_indexed(gfx)) return;
for (i32 y = 0; y < pxl8_gfx_get_height(gfx); y++) {
for (i32 x = 0; x < pxl8_gfx_get_width(gfx); x++) {
f32 v1 = sinf(x * scale1 + time);
f32 v2 = sinf(y * scale1 + time * 0.7f);
f32 v3 = sinf((x + y) * scale2 + time * 1.3f);
f32 v4 = sinf(sqrtf(x * x + y * y) * 0.05f + time * 0.5f);
f32 v = v1 + v2 + v3 + v4;
f32 normalized = (1.0f + v / 4.0f) * 0.5f;
if (normalized < 0.0f) normalized = 0.0f;
if (normalized > 1.0f) normalized = 1.0f;
u8 color = palette_offset + (u8)(15.0f * normalized);
pxl8_pixel(gfx, x, y, color);
}
}
}
void pxl8_vfx_raster_bars(pxl8_gfx* gfx, pxl8_raster_bar* bars, u32 bar_count, f32 time) {
if (!gfx || !bars) return;
for (u32 i = 0; i < bar_count; i++) {
pxl8_raster_bar* bar = &bars[i];
if (bar->height <= 1) continue;
f32 y = bar->base_y + bar->amplitude * sinf(time * bar->speed + bar->phase);
i32 y_int = (i32)y;
for (i32 dy = 0; dy < bar->height; dy++) {
f32 position = (f32)dy / (f32)(bar->height - 1);
f32 distance_from_center = fabsf(position - 0.5f) * 2.0f;
u8 color_idx;
if (distance_from_center < 0.3f) {
color_idx = bar->fade_color;
} else if (distance_from_center < 0.6f) {
color_idx = bar->fade_color - 1;
} else if (distance_from_center < 0.8f) {
color_idx = bar->color;
} else {
color_idx = bar->color - 1;
}
pxl8_rect_fill(gfx, 0, y_int + dy, pxl8_gfx_get_width(gfx), 1, color_idx);
}
}
}
void pxl8_vfx_rotozoom(pxl8_gfx* gfx, f32 angle, f32 zoom, i32 cx, i32 cy) {
if (!gfx || !pxl8_gfx_get_framebuffer_indexed(gfx)) return;
f32 cos_a = cosf(angle);
f32 sin_a = sinf(angle);
u8* temp_buffer = (u8*)malloc(pxl8_gfx_get_width(gfx) * pxl8_gfx_get_height(gfx));
if (!temp_buffer) return;
memcpy(temp_buffer, pxl8_gfx_get_framebuffer_indexed(gfx), pxl8_gfx_get_width(gfx) * pxl8_gfx_get_height(gfx));
for (i32 y = 0; y < pxl8_gfx_get_height(gfx); y++) {
for (i32 x = 0; x < pxl8_gfx_get_width(gfx); x++) {
f32 dx = x - cx;
f32 dy = y - cy;
f32 src_x = cx + (dx * cos_a - dy * sin_a) / zoom;
f32 src_y = cy + (dx * sin_a + dy * cos_a) / zoom;
i32 sx = (i32)src_x;
i32 sy = (i32)src_y;
if (sx >= 0 && sx < pxl8_gfx_get_width(gfx) && sy >= 0 && sy < pxl8_gfx_get_height(gfx)) {
pxl8_gfx_get_framebuffer_indexed(gfx)[y * pxl8_gfx_get_width(gfx) + x] = temp_buffer[sy * pxl8_gfx_get_width(gfx) + sx];
}
}
}
free(temp_buffer);
}
void pxl8_vfx_tunnel(pxl8_gfx* gfx, f32 time, f32 speed, f32 twist) {
if (!gfx || !pxl8_gfx_get_framebuffer_indexed(gfx)) return;
f32 cx = pxl8_gfx_get_width(gfx) / 2.0f;
f32 cy = pxl8_gfx_get_height(gfx) / 2.0f;
u32 palette_size = pxl8_gfx_get_palette_size(gfx);
if (palette_size == 0) palette_size = 256;
for (i32 y = 0; y < pxl8_gfx_get_height(gfx); y++) {
for (i32 x = 0; x < pxl8_gfx_get_width(gfx); x++) {
f32 dx = x - cx;
f32 dy = y - cy;
f32 dist = sqrtf(dx * dx + dy * dy);
if (dist > 1.0f) {
f32 angle = atan2f(dy, dx);
f32 u = angle * 0.159f + twist * sinf(time * 0.5f);
f32 v = 256.0f / dist + time * speed;
u8 tx = (u8)((i32)(u * 256.0f) & 0xFF);
u8 ty = (u8)((i32)(v * 256.0f) & 0xFF);
u8 pattern = tx ^ ty;
u8 color = (pattern / 8) % palette_size;
pxl8_pixel(gfx, x, y, color);
}
}
}
}
void pxl8_vfx_water_ripple(pxl8_gfx* gfx, f32* height_map, i32 drop_x, i32 drop_y, f32 damping) {
if (!gfx || !height_map) return;
i32 w = pxl8_gfx_get_width(gfx);
i32 h = pxl8_gfx_get_height(gfx);
static f32* prev_height = NULL;
if (!prev_height) {
prev_height = (f32*)calloc(w * h, sizeof(f32));
if (!prev_height) return;
}
if (drop_x >= 0 && drop_x < w && drop_y >= 0 && drop_y < h) {
height_map[drop_y * w + drop_x] = 255.0f;
}
for (i32 y = 1; y < h - 1; y++) {
for (i32 x = 1; x < w - 1; x++) {
i32 idx = y * w + x;
f32 sum = height_map[idx - w] + height_map[idx + w] +
height_map[idx - 1] + height_map[idx + 1];
f32 avg = sum / 2.0f;
prev_height[idx] = (avg - prev_height[idx]) * damping;
}
}
f32* temp = height_map;
height_map = prev_height;
prev_height = temp;
}
pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng) {
pxl8_particles* particles = calloc(1, sizeof(pxl8_particles));
if (!particles) return NULL;
particles->particles = calloc(max_count, sizeof(pxl8_particle));
if (!particles->particles) {
free(particles);
return NULL;
}
particles->rng = rng;
particles->max_count = max_count;
particles->drag = 0.98f;
particles->gravity_y = 100.0f;
particles->spawn_rate = 10.0f;
return particles;
}
void pxl8_particles_destroy(pxl8_particles* particles) {
if (!particles) return;
free(particles->particles);
free(particles);
}
void pxl8_particles_clear(pxl8_particles* particles) {
if (!particles || !particles->particles) return;
for (u32 i = 0; i < particles->max_count; i++) {
particles->particles[i].life = 0;
particles->particles[i].flags = 0;
}
particles->alive_count = 0;
particles->spawn_timer = 0;
}
void pxl8_particles_emit(pxl8_particles* particles, u32 count) {
if (!particles || !particles->particles) return;
for (u32 i = 0; i < count && particles->alive_count < particles->max_count; i++) {
for (u32 j = 0; j < particles->max_count; j++) {
if (particles->particles[j].life <= 0) {
pxl8_particle* p = &particles->particles[j];
p->life = 1.0f;
p->max_life = 1.0f;
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;
p->ay = particles->gravity_y;
p->az = 0;
p->color = p->start_color = p->end_color = 15;
p->size = 1.0f;
p->angle = 0;
p->spin = 0;
p->flags = 1;
if (particles->spawn_fn) {
particles->spawn_fn(particles, p);
}
particles->alive_count++;
break;
}
}
}
}
void pxl8_particles_render(pxl8_particles* particles, pxl8_gfx* gfx) {
if (!particles || !particles->particles || !gfx) return;
for (u32 i = 0; i < particles->max_count; i++) {
pxl8_particle* p = &particles->particles[i];
if (p->life > 0 && p->flags) {
if (particles->render_fn) {
particles->render_fn(gfx, p, particles->userdata);
} else {
i32 x = (i32)p->x;
i32 y = (i32)p->y;
if (x >= 0 && x < pxl8_gfx_get_width(gfx) && y >= 0 && y < pxl8_gfx_get_height(gfx)) {
pxl8_pixel(gfx, x, y, p->color);
}
}
}
}
}
void pxl8_particles_update(pxl8_particles* particles, f32 dt) {
if (!particles || !particles->particles) return;
if (particles->spawn_rate > 0.0f) {
particles->spawn_timer += dt;
f32 spawn_interval = 1.0f / particles->spawn_rate;
u32 max_spawns_per_frame = particles->max_count / 10;
if (max_spawns_per_frame < 1) max_spawns_per_frame = 1;
u32 spawn_count = 0;
while (particles->spawn_timer >= spawn_interval && spawn_count < max_spawns_per_frame) {
pxl8_particles_emit(particles, 1);
particles->spawn_timer -= spawn_interval;
spawn_count++;
}
}
for (u32 i = 0; i < particles->max_count; i++) {
pxl8_particle* p = &particles->particles[i];
if (p->life > 0) {
if (particles->update_fn) {
particles->update_fn(p, dt, particles->userdata);
} else {
p->vx += p->ax * dt;
p->vy += p->ay * dt;
p->vz += p->az * dt;
p->vx *= particles->drag;
p->vy *= particles->drag;
p->vz *= particles->drag;
p->x += p->vx * dt;
p->y += p->vy * dt;
p->z += p->vz * dt;
p->angle += p->spin * dt;
}
p->life -= dt / p->max_life;
if (p->life <= 0) {
p->flags = 0;
particles->alive_count--;
}
}
}
}
void pxl8_vfx_explosion(pxl8_particles* particles, i32 x, i32 y, u32 color, f32 force) {
if (!particles) return;
particles->x = x;
particles->y = y;
particles->spread_x = particles->spread_y = 2.0f;
particles->gravity_x = 0;
particles->gravity_y = 200.0f;
particles->drag = 0.95f;
particles->update_fn = NULL;
for (u32 i = 0; i < 50 && i < particles->max_count; i++) {
pxl8_particle* p = &particles->particles[i];
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_rng_f32(particles->rng);
p->color = color;
p->flags = 1;
}
}
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_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) {
(void)userdata;
p->vx += p->ax * dt;
p->vy += p->ay * dt;
p->vz += p->az * dt;
p->x += p->vx * dt;
p->y += p->vy * dt;
p->z += p->vz * dt;
f32 life_ratio = 1.0f - (p->life / p->max_life);
if (p->start_color >= p->end_color) {
u8 color_range = p->start_color - p->end_color;
p->color = p->start_color - (u8)(life_ratio * color_range);
} else {
u8 color_range = p->end_color - p->start_color;
p->color = p->start_color + (u8)(life_ratio * color_range);
}
}
void pxl8_vfx_fire(pxl8_particles* particles, i32 x, i32 y, i32 width, u8 palette_start) {
if (!particles) return;
particles->x = x;
particles->y = y;
particles->spread_x = width;
particles->spread_y = 4.0f;
particles->gravity_x = 0;
particles->gravity_y = -100.0f;
particles->drag = 0.97f;
particles->spawn_rate = 120.0f;
particles->spawn_fn = fire_spawn;
particles->update_fn = fire_update;
particles->userdata = (void*)(uintptr_t)palette_start;
}
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_rng_f32(particles->rng) * 100.0f;
}
void pxl8_vfx_rain(pxl8_particles* particles, i32 width, f32 wind) {
if (!particles) return;
particles->x = width / 2.0f;
particles->y = -10;
particles->spread_x = width;
particles->spread_y = 0;
particles->gravity_x = wind;
particles->gravity_y = 300.0f;
particles->drag = 1.0f;
particles->spawn_rate = 100.0f;
particles->spawn_fn = rain_spawn;
particles->update_fn = NULL;
}
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_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) {
if (!particles) return;
particles->x = x;
particles->y = y;
particles->spread_x = 5.0f;
particles->spread_y = 5.0f;
particles->gravity_x = 0;
particles->gravity_y = -50.0f;
particles->drag = 0.96f;
particles->spawn_rate = 20.0f;
particles->spawn_fn = smoke_spawn;
particles->update_fn = NULL;
particles->userdata = (void*)(uintptr_t)color;
}
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_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) {
if (!particles) return;
particles->x = width / 2.0f;
particles->y = -10;
particles->spread_x = width;
particles->spread_y = 0;
particles->gravity_x = wind;
particles->gravity_y = 30.0f;
particles->drag = 1.0f;
particles->spawn_rate = 30.0f;
particles->spawn_fn = snow_spawn;
particles->update_fn = NULL;
}
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_rng_f32(particles->rng) * 1.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;
}
void pxl8_vfx_sparks(pxl8_particles* particles, i32 x, i32 y, u32 color) {
if (!particles) return;
particles->x = x;
particles->y = y;
particles->spread_x = 2.0f;
particles->spread_y = 2.0f;
particles->gravity_x = 0;
particles->gravity_y = 100.0f;
particles->drag = 0.97f;
particles->spawn_rate = 40.0f;
particles->spawn_fn = sparks_spawn;
particles->update_fn = NULL;
particles->userdata = (void*)(uintptr_t)color;
}
void pxl8_vfx_starfield(pxl8_particles* particles, f32 speed, f32 spread) {
if (!particles) return;
particles->spread_x = particles->spread_y = spread;
particles->gravity_x = particles->gravity_y = 0;
particles->drag = 1.0f;
particles->spawn_rate = 0;
particles->update_fn = NULL;
for (u32 i = 0; i < particles->max_count; i++) {
pxl8_particle* p = &particles->particles[i];
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 + pxl8_rng_range(particles->rng, 0, 8);
p->flags = 1;
}
}

View file

@ -1,62 +0,0 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_rng.h"
#include "pxl8_types.h"
typedef struct pxl8_particles pxl8_particles;
typedef struct pxl8_particle {
f32 angle;
f32 ax, ay, az;
u32 color;
u32 end_color;
u8 flags;
f32 life;
f32 max_life;
f32 size;
f32 spin;
u32 start_color;
f32 vx, vy, vz;
f32 x, y, z;
} pxl8_particle;
typedef struct pxl8_raster_bar {
f32 amplitude;
f32 base_y;
u32 color;
u32 fade_color;
i32 height;
f32 phase;
f32 speed;
} pxl8_raster_bar;
#ifdef __cplusplus
extern "C" {
#endif
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);
void pxl8_particles_emit(pxl8_particles* particles, u32 count);
void pxl8_particles_render(pxl8_particles* particles, pxl8_gfx* gfx);
void pxl8_particles_update(pxl8_particles* particles, f32 dt);
void pxl8_vfx_explosion(pxl8_particles* particles, i32 x, i32 y, u32 color, f32 force);
void pxl8_vfx_fire(pxl8_particles* particles, i32 x, i32 y, i32 width, u8 palette_start);
void pxl8_vfx_rain(pxl8_particles* particles, i32 width, f32 wind);
void pxl8_vfx_smoke(pxl8_particles* particles, i32 x, i32 y, u8 color);
void pxl8_vfx_snow(pxl8_particles* particles, i32 width, f32 wind);
void pxl8_vfx_sparks(pxl8_particles* particles, i32 x, i32 y, u32 color);
void pxl8_vfx_starfield(pxl8_particles* particles, f32 speed, f32 spread);
void pxl8_vfx_plasma(pxl8_gfx* gfx, f32 time, f32 scale1, f32 scale2, u8 palette_offset);
void pxl8_vfx_raster_bars(pxl8_gfx* gfx, pxl8_raster_bar* bars, u32 bar_count, f32 time);
void pxl8_vfx_rotozoom(pxl8_gfx* gfx, f32 angle, f32 zoom, i32 cx, i32 cy);
void pxl8_vfx_tunnel(pxl8_gfx* gfx, f32 time, f32 speed, f32 twist);
void pxl8_vfx_water_ripple(pxl8_gfx* gfx, f32* height_map, i32 drop_x, i32 drop_y, f32 damping);
#ifdef __cplusplus
}
#endif

View file

@ -1,399 +0,0 @@
#include "pxl8_world.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_bsp.h"
#include "pxl8_gen.h"
#include "pxl8_log.h"
#include "pxl8_math.h"
struct pxl8_world {
pxl8_bsp bsp;
bool loaded;
bool wireframe;
u32 wireframe_color;
};
pxl8_world* pxl8_world_create(void) {
pxl8_world* world = (pxl8_world*)calloc(1, sizeof(pxl8_world));
if (!world) {
pxl8_error("Failed to allocate world");
return NULL;
}
world->loaded = false;
world->wireframe_color = 15;
return world;
}
void pxl8_world_destroy(pxl8_world* world) {
if (!world) return;
if (world->loaded) {
pxl8_bsp_destroy(&world->bsp);
}
free(world);
}
pxl8_result pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params) {
if (!world || !gfx || !params) {
pxl8_error("Invalid arguments to pxl8_world_generate");
return PXL8_ERROR_INVALID_ARGUMENT;
}
if (world->loaded) {
pxl8_bsp_destroy(&world->bsp);
world->loaded = false;
}
memset(&world->bsp, 0, sizeof(pxl8_bsp));
pxl8_result result = pxl8_procgen(&world->bsp, params);
if (result != PXL8_OK) {
pxl8_error("Failed to generate world: %d", result);
pxl8_bsp_destroy(&world->bsp);
return result;
}
world->loaded = true;
return PXL8_OK;
}
pxl8_result pxl8_world_load(pxl8_world* world, const char* path) {
if (!world || !path) return PXL8_ERROR_INVALID_ARGUMENT;
if (world->loaded) {
pxl8_bsp_destroy(&world->bsp);
world->loaded = false;
}
memset(&world->bsp, 0, sizeof(pxl8_bsp));
pxl8_result result = pxl8_bsp_load(path, &world->bsp);
if (result != PXL8_OK) {
pxl8_error("Failed to load world: %s", path);
return result;
}
world->loaded = true;
pxl8_info("Loaded world: %s", path);
return PXL8_OK;
}
void pxl8_world_unload(pxl8_world* world) {
if (!world || !world->loaded) return;
pxl8_bsp_destroy(&world->bsp);
world->loaded = false;
}
pxl8_result pxl8_world_apply_textures(pxl8_world* world, const pxl8_world_texture* textures, u32 count) {
if (!world || !world->loaded || !textures || count == 0) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_bsp* bsp = &world->bsp;
u32 max_texinfo = count * 6;
bsp->texinfo = calloc(max_texinfo, sizeof(pxl8_bsp_texinfo));
if (!bsp->texinfo) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
bsp->num_texinfo = 0;
for (u32 face_idx = 0; face_idx < bsp->num_faces; face_idx++) {
pxl8_bsp_face* face = &bsp->faces[face_idx];
pxl8_vec3 normal = bsp->planes[face->plane_id].normal;
u32 matched_texture_idx = count;
for (u32 tex_idx = 0; tex_idx < count; tex_idx++) {
if (textures[tex_idx].rule && textures[tex_idx].rule(&normal, face, bsp)) {
matched_texture_idx = tex_idx;
break;
}
}
if (matched_texture_idx >= count) {
pxl8_warn("No texture rule matched for face %u", face_idx);
continue;
}
const pxl8_world_texture* matched = &textures[matched_texture_idx];
pxl8_vec3 u_axis, v_axis;
if (fabsf(normal.y) > 0.9f) {
u_axis = (pxl8_vec3){1.0f, 0.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 0.0f, 1.0f};
} else if (fabsf(normal.x) > 0.7f) {
u_axis = (pxl8_vec3){0.0f, 1.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 0.0f, 1.0f};
} else {
u_axis = (pxl8_vec3){1.0f, 0.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 1.0f, 0.0f};
}
u32 texinfo_idx = bsp->num_texinfo;
bool found_existing = false;
for (u32 i = 0; i < bsp->num_texinfo; i++) {
if (strcmp(bsp->texinfo[i].name, matched->name) == 0 &&
bsp->texinfo[i].miptex == matched->texture_id &&
bsp->texinfo[i].u_axis.x == u_axis.x &&
bsp->texinfo[i].u_axis.y == u_axis.y &&
bsp->texinfo[i].u_axis.z == u_axis.z &&
bsp->texinfo[i].v_axis.x == v_axis.x &&
bsp->texinfo[i].v_axis.y == v_axis.y &&
bsp->texinfo[i].v_axis.z == v_axis.z) {
texinfo_idx = i;
found_existing = true;
break;
}
}
if (!found_existing) {
if (bsp->num_texinfo >= max_texinfo) {
pxl8_error("Too many unique texinfo entries");
return PXL8_ERROR_OUT_OF_MEMORY;
}
memcpy(bsp->texinfo[texinfo_idx].name, matched->name, sizeof(bsp->texinfo[texinfo_idx].name));
bsp->texinfo[texinfo_idx].name[sizeof(bsp->texinfo[texinfo_idx].name) - 1] = '\0';
bsp->texinfo[texinfo_idx].miptex = matched->texture_id;
bsp->texinfo[texinfo_idx].u_offset = 0.0f;
bsp->texinfo[texinfo_idx].v_offset = 0.0f;
bsp->texinfo[texinfo_idx].u_axis = u_axis;
bsp->texinfo[texinfo_idx].v_axis = v_axis;
bsp->num_texinfo++;
}
face->texinfo_id = texinfo_idx;
}
pxl8_info("Applied %u textures to %u faces, created %u texinfo entries",
count, bsp->num_faces, bsp->num_texinfo);
return PXL8_OK;
}
bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius) {
if (!world || !world->loaded) return false;
const pxl8_bsp* bsp = &world->bsp;
for (u32 i = 0; i < bsp->num_faces; i++) {
const pxl8_bsp_face* face = &bsp->faces[i];
const pxl8_bsp_plane* plane = &bsp->planes[face->plane_id];
if (fabsf(plane->normal.y) > 0.7f) {
continue;
}
f32 dist = plane->normal.x * pos.x +
plane->normal.y * pos.y +
plane->normal.z * pos.z - plane->dist;
if (fabsf(dist) > radius) {
continue;
}
pxl8_vec3 closest_point = {
pos.x - plane->normal.x * dist,
pos.y - plane->normal.y * dist,
pos.z - plane->normal.z * dist
};
if (closest_point.x < face->aabb_min.x - radius || closest_point.x > face->aabb_max.x + radius ||
closest_point.y < face->aabb_min.y - radius || closest_point.y > face->aabb_max.y + radius ||
closest_point.z < face->aabb_min.z - radius || closest_point.z > face->aabb_max.z + radius) {
continue;
}
return true;
}
return false;
}
bool pxl8_world_is_loaded(const pxl8_world* world) {
return world && world->loaded;
}
pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
if (!world || !world->loaded) return to;
const pxl8_bsp* bsp = &world->bsp;
pxl8_vec3 pos = to;
pxl8_vec3 original_velocity = {to.x - from.x, to.y - from.y, to.z - from.z};
pxl8_vec3 clip_planes[5];
u32 num_planes = 0;
const f32 edge_epsilon = 1.2f;
const f32 radius_min = -radius + edge_epsilon;
const f32 radius_max = radius - edge_epsilon;
for (i32 iteration = 0; iteration < 4; iteration++) {
bool collided = false;
for (u32 i = 0; i < bsp->num_faces; i++) {
const pxl8_bsp_face* face = &bsp->faces[i];
const pxl8_bsp_plane* plane = &bsp->planes[face->plane_id];
if (fabsf(plane->normal.y) > 0.7f) continue;
f32 dist = plane->normal.x * pos.x +
plane->normal.y * pos.y +
plane->normal.z * pos.z - plane->dist;
f32 abs_dist = fabsf(dist);
if (abs_dist > radius) continue;
pxl8_vec3 closest_point = {
pos.x - plane->normal.x * dist,
pos.y - plane->normal.y * dist,
pos.z - plane->normal.z * dist
};
if (closest_point.x < face->aabb_min.x + radius_min || closest_point.x > face->aabb_max.x + radius_max ||
closest_point.y < face->aabb_min.y + radius_min || closest_point.y > face->aabb_max.y + radius_max ||
closest_point.z < face->aabb_min.z + radius_min || closest_point.z > face->aabb_max.z + radius_max) {
continue;
}
f32 penetration = radius - abs_dist;
if (penetration > 0.01f) {
pxl8_vec3 push_dir;
if (dist < 0) {
push_dir.x = -plane->normal.x;
push_dir.y = -plane->normal.y;
push_dir.z = -plane->normal.z;
} else {
push_dir.x = plane->normal.x;
push_dir.y = plane->normal.y;
push_dir.z = plane->normal.z;
}
bool is_new_plane = true;
for (u32 p = 0; p < num_planes; p++) {
if (pxl8_vec3_dot(push_dir, clip_planes[p]) > 0.99f) {
is_new_plane = false;
break;
}
}
if (is_new_plane && num_planes < 5) {
clip_planes[num_planes++] = push_dir;
}
pos.x += push_dir.x * penetration;
pos.y += push_dir.y * penetration;
pos.z += push_dir.z * penetration;
collided = true;
}
}
if (!collided) {
break;
}
if (num_planes >= 3) {
break;
}
}
if (num_planes == 2) {
f32 orig_vel_len_sq = original_velocity.x * original_velocity.x +
original_velocity.y * original_velocity.y +
original_velocity.z * original_velocity.z;
if (orig_vel_len_sq > 0.000001f) {
f32 vdot0 = pxl8_vec3_dot(original_velocity, clip_planes[0]);
f32 vdot1 = pxl8_vec3_dot(original_velocity, clip_planes[1]);
f32 dot0 = fabsf(vdot0);
f32 dot1 = fabsf(vdot1);
pxl8_vec3 slide_vel;
if (dot0 < dot1) {
slide_vel.x = original_velocity.x - clip_planes[0].x * vdot0;
slide_vel.y = original_velocity.y - clip_planes[0].y * vdot0;
slide_vel.z = original_velocity.z - clip_planes[0].z * vdot0;
} else {
slide_vel.x = original_velocity.x - clip_planes[1].x * vdot1;
slide_vel.y = original_velocity.y - clip_planes[1].y * vdot1;
slide_vel.z = original_velocity.z - clip_planes[1].z * vdot1;
}
f32 slide_len = sqrtf(slide_vel.x * slide_vel.x + slide_vel.y * slide_vel.y + slide_vel.z * slide_vel.z);
if (slide_len > 0.01f) {
pos.x += slide_vel.x;
pos.y += slide_vel.y;
pos.z += slide_vel.z;
pxl8_vec3 crease_dir = pxl8_vec3_cross(clip_planes[0], clip_planes[1]);
f32 crease_len = pxl8_vec3_length(crease_dir);
if (crease_len > 0.01f && fabsf(crease_dir.y / crease_len) > 0.9f) {
f32 bias0 = pxl8_vec3_dot(original_velocity, clip_planes[0]);
f32 bias1 = pxl8_vec3_dot(original_velocity, clip_planes[1]);
if (bias0 < 0 && bias1 < 0) {
const f32 corner_push = 0.1f;
pxl8_vec3 push_away = {
clip_planes[0].x + clip_planes[1].x,
clip_planes[0].y + clip_planes[1].y,
clip_planes[0].z + clip_planes[1].z
};
f32 push_len_sq = push_away.x * push_away.x + push_away.y * push_away.y + push_away.z * push_away.z;
if (push_len_sq > 0.000001f) {
f32 inv_push_len = corner_push / sqrtf(push_len_sq);
pos.x += push_away.x * inv_push_len;
pos.y += push_away.y * inv_push_len;
pos.z += push_away.z * inv_push_len;
}
}
}
}
}
}
f32 horizontal_movement_sq = (pos.x - from.x) * (pos.x - from.x) +
(pos.z - from.z) * (pos.z - from.z);
f32 desired_horizontal_sq = (to.x - from.x) * (to.x - from.x) +
(to.z - from.z) * (to.z - from.z);
if (desired_horizontal_sq > 0.01f && horizontal_movement_sq < desired_horizontal_sq * 0.25f) {
const f32 max_step_height = 0.4f;
pxl8_vec3 step_up = pos;
step_up.y += max_step_height;
if (!pxl8_world_check_collision(world, step_up, radius)) {
pxl8_vec3 step_forward = {
step_up.x + (to.x - pos.x),
step_up.y,
step_up.z + (to.z - pos.z)
};
if (!pxl8_world_check_collision(world, step_forward, radius)) {
pos = step_forward;
}
}
}
return pos;
}
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) {
if (!world || !gfx || !world->loaded) return;
if (world->wireframe) {
pxl8_bsp_render_wireframe(gfx, &world->bsp, camera_pos, world->wireframe_color);
} else {
pxl8_bsp_render_textured(gfx, &world->bsp, camera_pos);
}
}

View file

@ -1,38 +0,0 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_gen.h"
#include "pxl8_gfx.h"
#include "pxl8_math.h"
#include "pxl8_types.h"
typedef struct pxl8_world pxl8_world;
typedef bool (*pxl8_texture_rule)(const pxl8_vec3* normal, const pxl8_bsp_face* face, const pxl8_bsp* bsp);
typedef struct pxl8_world_texture {
char name[16];
u32 texture_id;
pxl8_texture_rule rule;
} pxl8_world_texture;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_world* pxl8_world_create(void);
void pxl8_world_destroy(pxl8_world* world);
pxl8_result pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params);
pxl8_result pxl8_world_load(pxl8_world* world, const char* path);
void pxl8_world_unload(pxl8_world* world);
pxl8_result pxl8_world_apply_textures(pxl8_world* world, const pxl8_world_texture* textures, u32 count);
bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius);
bool pxl8_world_is_loaded(const pxl8_world* world);
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos);
pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
#ifdef __cplusplus
}
#endif