diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..966a61f --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,142 @@ +#!/bin/bash +# Runs every test tier and exits nonzero if any of them fail. +# +# ./run_tests.sh # everything that can run here +# ./run_tests.sh unit # compile-time tests only (no display needed) +# ./run_tests.sh engine # engine tiers only +# ./run_tests.sh game # game tiers only +# +# Tiers: +# test_engine engine unit tests compile-time #run, builds against test_game/ +# test_game game unit tests compile-time #run, builds against game/ +# test_exe_engine engine exe tests builds then runs ./first against test_game/ +# test_exe_game game exe tests builds then runs ./first against game/ +# +# The unit tiers report through the compiler's exit code. The exe tiers run the +# real app, so they need a display and are wrapped in a hard timeout. + +set -uo pipefail +cd "$(dirname "$0")" + +JAI="${JAI:-$HOME/bin/jai/bin/jai-linux}" +EXE_TIMEOUT="${EXE_TIMEOUT:-180}" # seconds per exe-test binary, kills a true freeze + +FILTER="${1:-all}" + +if [ ! -x "$JAI" ]; then + echo "error: Jai compiler not found at '$JAI' (override with JAI=/path/to/jai)" >&2 + exit 1 +fi + +# The game/ directory is not in the repo, so the game tiers are skipped rather +# than failed on a checkout that does not have it. +HAVE_GAME=0 +[ -f game/game.jai ] && HAVE_GAME=1 + +# Exe tests open a real window. Prefer a live display, fall back to Xvfb. +XVFB=() +NO_DISPLAY=0 +if [ -z "${DISPLAY:-}" ]; then + if command -v xvfb-run >/dev/null 2>&1; then + XVFB=(xvfb-run -a) + else + NO_DISPLAY=1 + fi +fi + +PASSED=(); FAILED=(); SKIPPED=() + +want() { + case "$FILTER" in + all) return 0 ;; + unit) [[ "$1" != *exe* ]] ;; + engine) [[ "$1" == *engine* ]] ;; + game) [[ "$1" == *game* && "$1" != *engine* ]] ;; + *) echo "error: unknown filter '$FILTER' (use all|unit|engine|game)" >&2; exit 1 ;; + esac +} + +run_unit_tier() { + local flag="$1" + echo "" + echo "==============================================================" + echo " $flag (compile-time)" + echo "==============================================================" + if "$JAI" first.jai - "$flag"; then + PASSED+=("$flag") + else + FAILED+=("$flag") + fi +} + +run_exe_tier() { + local flag="$1" + echo "" + echo "==============================================================" + echo " $flag (builds, then runs the app)" + echo "==============================================================" + + if ! "$JAI" first.jai - "$flag"; then + echo "[$flag] build failed" + FAILED+=("$flag (build)") + return + fi + + timeout --foreground "$EXE_TIMEOUT" "${XVFB[@]}" ./first + local status=$? + if [ $status -eq 0 ]; then + PASSED+=("$flag") + elif [ $status -eq 124 ]; then + echo "[$flag] hung and was killed after ${EXE_TIMEOUT}s" + FAILED+=("$flag (hung)") + else + echo "[$flag] exited $status" + FAILED+=("$flag") + fi +} + +for flag in test_engine test_game; do + want "$flag" || continue + if [ "$flag" = "test_game" ] && [ "$HAVE_GAME" -eq 0 ]; then + SKIPPED+=("$flag (no game/ directory)") + continue + fi + run_unit_tier "$flag" +done + +for flag in test_exe_engine test_exe_game; do + want "$flag" || continue + if [ "$NO_DISPLAY" -eq 1 ]; then + SKIPPED+=("$flag (no DISPLAY and no xvfb-run)") + continue + fi + if [ "$flag" = "test_exe_game" ] && [ "$HAVE_GAME" -eq 0 ]; then + SKIPPED+=("$flag (no game/ directory)") + continue + fi + run_exe_tier "$flag" +done + +echo "" +echo "==============================================================" +echo " Summary" +echo "==============================================================" +for t in ${PASSED[@]+"${PASSED[@]}"}; do echo " PASS $t"; done +for t in ${SKIPPED[@]+"${SKIPPED[@]}"}; do echo " SKIP $t"; done +for t in ${FAILED[@]+"${FAILED[@]}"}; do echo " FAIL $t"; done + +if [ ${#FAILED[@]} -ne 0 ]; then + echo "" + echo "${#FAILED[@]} tier(s) failed." + exit 1 +fi + +# A run where everything was skipped is not a pass. +if [ ${#PASSED[@]} -eq 0 ]; then + echo "" + echo "Nothing ran." + exit 1 +fi + +echo "" +echo "All ${#PASSED[@]} tier(s) passed." diff --git a/src/editor/editor.jai b/src/editor/editor.jai index 7fd7068..9a289e5 100644 --- a/src/editor/editor.jai +++ b/src/editor/editor.jai @@ -2,6 +2,7 @@ #load "iprof.jai"; #load "trile_thumbnails.jai"; #load "picker.jai"; + #load "gizmo.jai"; #load "trile_editor.jai"; #load "level_editor.jai"; #load "particle_editor.jai"; diff --git a/src/editor/gizmo.jai b/src/editor/gizmo.jai new file mode 100644 index 0000000..3636096 --- /dev/null +++ b/src/editor/gizmo.jai @@ -0,0 +1,455 @@ +// Blender-style gizmos: a translate gizmo with three RGB axis arrows plus three +// plane handles for two-axis drags, and a rotate gizmo with three RGB rings. +// Drawn with depth-ignoring overlay lines so they stay visible through terrain, +// and picked in world space so the pick thresholds scale with the gizmo itself. + +Gizmo_Handle :: enum { + NONE; + AXIS_X; + AXIS_Y; + AXIS_Z; + PLANE_YZ; // normal +X + PLANE_XZ; // normal +Y + PLANE_XY; // normal +Z + RING_X; // turns about +X + RING_Y; + RING_Z; +} + +Gizmo_State :: struct { + hover : Gizmo_Handle; + active : Gizmo_Handle; + + start_position : Vector3; // object position when the drag began + start_t : float; // axis drags: parameter along the axis at grab time + start_hit : Vector3; // plane drags: world point grabbed + start_angle : float; // ring drags: angle around the ring at grab time + applied_steps : int; // ring drags: quarter turns already handed to the caller +} + +// Gizmo length as a fraction of the camera distance, so it keeps a constant +// on-screen size. Everything else is expressed in units of that length. +GIZMO_SCALE :: 0.14; +GIZMO_PLANE_INNER :: 0.22; +GIZMO_PLANE_OUTER :: 0.52; +GIZMO_PICK_RADIUS :: 0.08; +// Below this |dot(plane normal, view dir)| the plane handle is edge-on: too thin +// to aim at, so it is neither drawn nor picked. +GIZMO_PLANE_MIN_FACING :: 0.15; +// Ring radius, and how far off it a click can land, both in units of gizmo length. +GIZMO_RING_RADIUS :: 1.0; +GIZMO_RING_PICK :: 0.14; +GIZMO_RING_SEGMENTS :: 48; +// Line widths, in pixels. +GIZMO_LINE_WIDTH :: 3.0; +GIZMO_FILL_WIDTH :: 2.0; + +// Runs one frame of the gizmo: hover, click-to-grab, drag, release, and drawing. +// Returns the dragged-to position; the caller owns snapping and writing it back. +gizmo_translate :: (using state: *Gizmo_State, position: Vector3, cam: Camera, ray: Ray) -> (position: Vector3, changed: bool) { + len := max(length(cam.position - position) * GIZMO_SCALE, 0.001); + pos := position; + changed := false; + + mouse := get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT); + pressed := (mouse & .START) != .NONE; + held := (mouse & .DOWN) != .NONE; + + if active == .NONE { + hover = gizmo_pick(position, len, cam, ray); + if hover != .NONE && pressed { + start_position = position; + if gizmo_is_plane(hover) { + ok, point := gizmo_plane_hit(position, hover, ray); + if ok { + start_hit = point; + active = hover; + } + } else { + t, _, _ := gizmo_closest_axis_t(ray, position, gizmo_axis(hover)); + start_t = t; + active = hover; + } + } + } else if !held { + active = .NONE; + } else { + if gizmo_is_plane(active) { + ok, point := gizmo_plane_hit(start_position, active, ray); + if ok { + pos = start_position + (point - start_hit); + changed = true; + } + } else { + axis := gizmo_axis(active); + t, _, _ := gizmo_closest_axis_t(ray, start_position, axis); + pos = start_position + axis * (t - start_t); + changed = true; + } + } + + // Drawn at the object's own position rather than the dragged-to one: the + // caller may snap what we hand back, and the gizmo has to sit on the object. + gizmo_draw(position, len, cam, ifx active != .NONE then active else hover); + return pos, changed; +} + +// One frame of the rotate gizmo. Entities only hold the 24 axis-aligned cube +// rotations, so a drag reports whole quarter turns: the returned basis is the +// (u, v, n) of the ring being dragged and 'steps' is how many turns of u toward +// v to apply since the last frame, which is usually zero. +gizmo_rotate :: (using state: *Gizmo_State, position: Vector3, cam: Camera, ray: Ray) -> (u: Vector3, v: Vector3, n: Vector3, steps: int) { + len := max(length(cam.position - position) * GIZMO_SCALE, 0.001); + + mouse := get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT); + pressed := (mouse & .START) != .NONE; + held := (mouse & .DOWN) != .NONE; + + steps := 0; + + if active == .NONE { + hover = gizmo_pick_ring(position, len, cam, ray); + if hover != .NONE && pressed { + ok, angle := gizmo_ring_angle(position, hover, ray); + if ok { + start_position = position; + start_angle = angle; + applied_steps = 0; + active = hover; + } + } + } else if !held { + active = .NONE; + } else { + ok, angle := gizmo_ring_angle(start_position, active, ray); + if ok { + // Wrap into (-180, 180] so passing the seam doesn't spin the object. + delta := angle - start_angle; + while delta > PI delta -= 2 * PI; + while delta < -PI delta += 2 * PI; + total := cast(int) floor(delta / (PI * 0.5) + 0.5); + steps = total - applied_steps; + applied_steps = total; + } + } + + gizmo_draw_rings(position, len, cam, ifx active != .NONE then active else hover); + + handle := ifx active != .NONE then active else Gizmo_Handle.RING_Y; + u, v := gizmo_plane_axes(handle); + return u, v, gizmo_axis(handle), steps; +} + +gizmo_axis :: (h: Gizmo_Handle) -> Vector3 { + if h == { + case .AXIS_X; #through; + case .PLANE_YZ; #through; + case .RING_X; return .{1, 0, 0}; + case .AXIS_Y; #through; + case .PLANE_XZ; #through; + case .RING_Y; return .{0, 1, 0}; + case; return .{0, 0, 1}; + } +} + +gizmo_is_plane :: (h: Gizmo_Handle) -> bool { + return h == .PLANE_YZ || h == .PLANE_XZ || h == .PLANE_XY; +} + +gizmo_is_ring :: (h: Gizmo_Handle) -> bool { + return h == .RING_X || h == .RING_Y || h == .RING_Z; +} + +// The two in-plane axes of a plane or ring handle; gizmo_axis gives the third +// (the plane normal, or the axis the ring turns about). +gizmo_plane_axes :: (h: Gizmo_Handle) -> (u: Vector3, v: Vector3) { + if h == { + case .PLANE_YZ; #through; + case .RING_X; return .{0, 1, 0}, .{0, 0, 1}; + case .PLANE_XZ; #through; + case .RING_Y; return .{1, 0, 0}, .{0, 0, 1}; + case; return .{1, 0, 0}, .{0, 1, 0}; + } +} + +// Handles are colored by their axis; plane handles by the axis they are +// perpendicular to, which is what gizmo_axis returns for them. +gizmo_color :: (h: Gizmo_Handle, highlighted: bool) -> Vector4 { + if highlighted then return .{1.0, 0.85, 0.2, 1.0}; + axis := gizmo_axis(h); + if axis.x != 0 then return .{0.95, 0.28, 0.30, 1.0}; + if axis.y != 0 then return .{0.40, 0.90, 0.32, 1.0}; + return .{0.30, 0.50, 0.98, 1.0}; +} + +#scope_file + +// Parameter along the line (origin + axis*t) of the point closest to the ray, +// plus that point and the distance between the two lines at closest approach. +gizmo_closest_axis_t :: (ray: Ray, origin: Vector3, axis: Vector3) -> (t: float, point: Vector3, dist: float) { + r := origin - ray.origin; + a := dot(axis, axis); + b := dot(axis, ray.direction); + e := dot(ray.direction, ray.direction); + c := dot(axis, r); + f := dot(ray.direction, r); + + denom := a*e - b*b; + if abs(denom) < 0.00001 then return 0, origin, 99999; // ray parallel to the axis + + t := (b*f - c*e) / denom; + s := (a*f - c*b) / denom; + pa := origin + axis * t; + pb := ray.origin + ray.direction * s; + return t, pa, length(pa - pb); +} + +gizmo_plane_hit :: (origin: Vector3, h: Gizmo_Handle, ray: Ray) -> (bool, Vector3) { + n := gizmo_axis(h); + denom := dot(n, ray.direction); + if abs(denom) < 0.0001 then return false, .{}; + t := dot(n, origin - ray.origin) / denom; + if t < 0 then return false, .{}; // plane is behind the camera + return true, ray.origin + ray.direction * t; +} + +// Which way each plane handle is offset from the origin: toward the camera, so +// the handles always sit in the octant facing the viewer. +gizmo_plane_signs :: (origin: Vector3, h: Gizmo_Handle, cam: Camera) -> (su: float, sv: float) { + u, v := gizmo_plane_axes(h); + to_cam := cam.position - origin; + su := ifx dot(u, to_cam) >= 0 then cast(float)1 else cast(float)-1; + sv := ifx dot(v, to_cam) >= 0 then cast(float)1 else cast(float)-1; + return su, sv; +} + +gizmo_pick :: (origin: Vector3, len: float, cam: Camera, ray: Ray) -> Gizmo_Handle { + // Planes first: they sit closer to the origin and never overlap the arrows, + // but grabbing a plane is the more common intent when they are near. + for h: Gizmo_Handle.[.PLANE_YZ, .PLANE_XZ, .PLANE_XY] { + n := gizmo_axis(h); + if abs(dot(n, normalize(cam.position - origin))) < GIZMO_PLANE_MIN_FACING then continue; + ok, point := gizmo_plane_hit(origin, h, ray); + if !ok then continue; + u, v := gizmo_plane_axes(h); + su, sv := gizmo_plane_signs(origin, h, cam); + d := point - origin; + cu := dot(d, u) * su; + cv := dot(d, v) * sv; + if cu >= GIZMO_PLANE_INNER * len && cu <= GIZMO_PLANE_OUTER * len + && cv >= GIZMO_PLANE_INNER * len && cv <= GIZMO_PLANE_OUTER * len { + return h; + } + } + + best := Gizmo_Handle.NONE; + best_dist := GIZMO_PICK_RADIUS * len; + for h: Gizmo_Handle.[.AXIS_X, .AXIS_Y, .AXIS_Z] { + t, _, dist := gizmo_closest_axis_t(ray, origin, gizmo_axis(h)); + if t < 0 || t > len then continue; + if dist < best_dist { + best_dist = dist; + best = h; + } + } + return best; +} + +gizmo_draw :: (origin: Vector3, len: float, cam: Camera, highlighted: Gizmo_Handle) { + for h: Gizmo_Handle.[.PLANE_YZ, .PLANE_XZ, .PLANE_XY] { + n := gizmo_axis(h); + if abs(dot(n, normalize(cam.position - origin))) < GIZMO_PLANE_MIN_FACING then continue; + u, v := gizmo_plane_axes(h); + su, sv := gizmo_plane_signs(origin, h, cam); + col := gizmo_color(h, h == highlighted); + gizmo_draw_plane(origin, u * su, v * sv, GIZMO_PLANE_INNER * len, GIZMO_PLANE_OUTER * len, col, h == highlighted); + } + + for h: Gizmo_Handle.[.AXIS_X, .AXIS_Y, .AXIS_Z] { + gizmo_draw_arrow(origin, gizmo_axis(h) * len, gizmo_color(h, h == highlighted)); + } +} + +gizmo_draw_arrow :: (origin: Vector3, vec: Vector3, col: Vector4) { + tip := origin + vec; + shaft := normalize(vec); + debug_line_overlay(origin, tip, col, GIZMO_LINE_WIDTH); + + head := length(vec) * 0.18; + back := tip - shaft * head; + perp : Vector3; + if abs(shaft.x) < 0.9 { + perp = normalize(cross(shaft, .{1, 0, 0})); + } else { + perp = normalize(cross(shaft, .{0, 1, 0})); + } + perp2 := cross(shaft, perp); + w1 := perp * (head * 0.4); + w2 := perp2 * (head * 0.4); + // Four fins plus a ring, so the head reads as a cone from any angle. + debug_line_overlay(back + w1, tip, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back - w1, tip, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back + w2, tip, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back - w2, tip, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back + w1, back + w2, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back + w2, back - w1, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back - w1, back - w2, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back - w2, back + w1, col, GIZMO_LINE_WIDTH); +} + +// A square in the (u, v) plane between inner and outer, outlined at full alpha +// and hatched at low alpha so it reads as a translucent pane. +gizmo_draw_plane :: (origin: Vector3, u: Vector3, v: Vector3, inner: float, outer: float, col: Vector4, filled: bool) { + p00 := origin + u * inner + v * inner; + p10 := origin + u * outer + v * inner; + p11 := origin + u * outer + v * outer; + p01 := origin + u * inner + v * outer; + debug_line_overlay(p00, p10, col, GIZMO_LINE_WIDTH); + debug_line_overlay(p10, p11, col, GIZMO_LINE_WIDTH); + debug_line_overlay(p11, p01, col, GIZMO_LINE_WIDTH); + debug_line_overlay(p01, p00, col, GIZMO_LINE_WIDTH); + + fill := col; + fill.w = ifx filled then 0.55 else 0.18; + // Enough hatch lines that the pane reads as filled rather than striped. + HATCH :: 14; + for i: 1..HATCH { + f := cast(float)i / cast(float)(HATCH + 1); + c := inner + (outer - inner) * f; + debug_line_overlay(origin + u * c + v * inner, origin + u * c + v * outer, fill, GIZMO_FILL_WIDTH); + } +} + +// Angle of the mouse ray's hit on the ring's plane, measured from u toward v. +gizmo_ring_angle :: (origin: Vector3, h: Gizmo_Handle, ray: Ray) -> (ok: bool, angle: float) { + ok, point := gizmo_plane_hit(origin, h, ray); + if !ok then return false, 0; + u, v := gizmo_plane_axes(h); + d := point - origin; + return true, atan2(dot(d, v), dot(d, u)); +} + +gizmo_pick_ring :: (origin: Vector3, len: float, cam: Camera, ray: Ray) -> Gizmo_Handle { + best := Gizmo_Handle.NONE; + best_dist := GIZMO_RING_PICK * len; + for h: Gizmo_Handle.[.RING_X, .RING_Y, .RING_Z] { + if !gizmo_ring_faces_camera(origin, h, cam) then continue; + ok, point := gizmo_plane_hit(origin, h, ray); + if !ok then continue; + // Distance from the ring itself, not from its centre. + off := abs(length(point - origin) - GIZMO_RING_RADIUS * len); + if off < best_dist { + best_dist = off; + best = h; + } + } + return best; +} + +// An edge-on ring is a line on screen: impossible to aim at, and its plane +// intersection shoots off to infinity, so skip it. +gizmo_ring_faces_camera :: (origin: Vector3, h: Gizmo_Handle, cam: Camera) -> bool { + return abs(dot(gizmo_axis(h), normalize(cam.position - origin))) >= GIZMO_PLANE_MIN_FACING; +} + +gizmo_draw_rings :: (origin: Vector3, len: float, cam: Camera, highlighted: Gizmo_Handle) { + for h: Gizmo_Handle.[.RING_X, .RING_Y, .RING_Z] { + is_hot := h == highlighted; + if !gizmo_ring_faces_camera(origin, h, cam) && !is_hot then continue; + u, v := gizmo_plane_axes(h); + col := gizmo_color(h, is_hot); + r := GIZMO_RING_RADIUS * len; + prev := origin + u * r; + for i: 1..GIZMO_RING_SEGMENTS { + a := (cast(float)i / cast(float)GIZMO_RING_SEGMENTS) * 2 * PI; + next := origin + u * (cos(a) * r) + v * (sin(a) * r); + debug_line_overlay(prev, next, col, GIZMO_LINE_WIDTH); + prev = next; + } + } +} + +#if FLAG_TEST_ENGINE { + gizmo_test_cam :: () -> Camera { + // Off the +X+Y+Z octant, so every plane handle faces the viewer and its + // signs are all positive. + cam : Camera; + cam.position = .{5, 5, 5}; + cam.target = .{0, 0, 0}; + return cam; + } + + test_gizmo_closest_axis :: () { + s := begin_suite("gizmo closest point on axis"); + // Vertical ray dropping onto the +X axis at x = 0.5. + ray := Ray.{ origin = .{0.5, 3, 0}, direction = .{0, -1, 0} }; + t, point, dist := gizmo_closest_axis_t(ray, .{0, 0, 0}, .{1, 0, 0}); + check(*s, "t is the distance along the axis", abs(t - 0.5) < 0.001); + check(*s, "closest point sits on the axis", abs(point.x - 0.5) < 0.001 && abs(point.y) < 0.001 && abs(point.z) < 0.001); + check(*s, "intersecting lines have 0 distance", dist < 0.001); + + // A ray offset in Z never meets the axis; the gap is that offset. + off := Ray.{ origin = .{0.5, 3, 0.25}, direction = .{0, -1, 0} }; + _, _, off_dist := gizmo_closest_axis_t(off, .{0, 0, 0}, .{1, 0, 0}); + check(*s, "offset ray reports its offset as distance", abs(off_dist - 0.25) < 0.001); + + // Parallel lines have no unique closest point. + par := Ray.{ origin = .{0, 3, 0}, direction = .{1, 0, 0} }; + _, _, par_dist := gizmo_closest_axis_t(par, .{0, 0, 0}, .{1, 0, 0}); + check(*s, "parallel ray is rejected", par_dist > 1000); + end_suite(s); + } + + test_gizmo_pick :: () { + s := begin_suite("gizmo handle picking"); + cam := gizmo_test_cam(); + len : float : 1.0; + + // Straight down onto the middle of the +X arrow. + axis_ray := Ray.{ origin = .{0.5, 3, 0}, direction = .{0, -1, 0} }; + check(*s, "aiming at the X arrow picks AXIS_X", gizmo_pick(.{0,0,0}, len, cam, axis_ray) == .AXIS_X); + + // Down onto the XZ pane, which spans 0.22..0.52 on both of its axes. + plane_ray := Ray.{ origin = .{0.35, 3, 0.35}, direction = .{0, -1, 0} }; + check(*s, "aiming at the XZ pane picks PLANE_XZ", gizmo_pick(.{0,0,0}, len, cam, plane_ray) == .PLANE_XZ); + + // Inside the pane's plane but short of its inner edge: no handle there. + gap_ray := Ray.{ origin = .{0.1, 3, 0.1}, direction = .{0, -1, 0} }; + check(*s, "the gap inside the panes picks nothing", gizmo_pick(.{0,0,0}, len, cam, gap_ray) == .NONE); + + // Past the end of every arrow. + miss_ray := Ray.{ origin = .{2, 3, 2}, direction = .{0, -1, 0} }; + check(*s, "aiming past the gizmo picks nothing", gizmo_pick(.{0,0,0}, len, cam, miss_ray) == .NONE); + + // Picking follows the gizmo, not the world origin. + moved := Vector3.{10, 4, -7}; + moved_ray := Ray.{ origin = moved + Vector3.{0.5, 3, 0}, direction = .{0, -1, 0} }; + check(*s, "picking is relative to the gizmo origin", gizmo_pick(moved, len, cam, moved_ray) == .AXIS_X); + end_suite(s); + } + + test_gizmo_plane_hit :: () { + s := begin_suite("gizmo plane intersection"); + // PLANE_XZ has a +Y normal, so it is the horizontal plane through the origin. + ray := Ray.{ origin = .{1, 4, 2}, direction = .{0, -1, 0} }; + ok, point := gizmo_plane_hit(.{0, 0, 0}, .PLANE_XZ, ray); + check(*s, "downward ray hits the XZ pane's plane", ok); + check(*s, "hit lands at the ray's XZ", abs(point.x - 1) < 0.001 && abs(point.z - 2) < 0.001); + check(*s, "hit lands on the plane", abs(point.y) < 0.001); + + // The plane passes through the gizmo origin, not the world origin. + shifted, sp := gizmo_plane_hit(.{0, 2.5, 0}, .PLANE_XZ, ray); + check(*s, "plane follows the gizmo origin", shifted && abs(sp.y - 2.5) < 0.001); + + away := Ray.{ origin = .{1, 4, 2}, direction = .{0, 1, 0} }; + no_hit, _ := gizmo_plane_hit(.{0, 0, 0}, .PLANE_XZ, away); + check(*s, "ray pointing away misses", !no_hit); + end_suite(s); + } + + #run { + test_gizmo_closest_axis(); + test_gizmo_pick(); + test_gizmo_plane_hit(); + } +} diff --git a/src/editor/level_editor.jai b/src/editor/level_editor.jai index d0de755..bbf626a 100644 --- a/src/editor/level_editor.jai +++ b/src/editor/level_editor.jai @@ -29,6 +29,7 @@ Level_Editor_Tool_Mode :: enum { LINE; INSPECTOR; VIEWER; + ENTITY; } current_tool_mode : Level_Editor_Tool_Mode = .POINT; @@ -59,6 +60,21 @@ inspector_rdm_roughness : int = 0; inspector_note_deactivate : bool; inspector_note_activate_next : bool; +// Entity tool: click to select, drag the gizmo to move or turn, right-click to +// place. The selection is held by id rather than index so removing an entity +// can't silently reselect another. +Entity_Gizmo_Mode :: enum { + MOVE; + ROTATE; +} + +entity_gizmo : Gizmo_State; +entity_gizmo_mode : Entity_Gizmo_Mode; +entity_selected_id : s64 = -1; +entity_add_type : s32 = -1; +entity_snap_enabled : bool = true; +entity_snap_step : float = 1.0; + editor_billboards_visible : bool = true; get_current_orientation :: () -> u8 { @@ -333,6 +349,8 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) { r.y += r.h; if keybind_button(r, "Inspector", .LEVEL_TOOL_INSPECTOR, *t_button_selectable(theme, current_tool_mode == .INSPECTOR)) then current_tool_mode = .INSPECTOR; r.y += r.h; + if keybind_button(r, "Entity", .LEVEL_TOOL_ENTITY, *t_button_selectable(theme, current_tool_mode == .ENTITY)) then current_tool_mode = .ENTITY; + r.y += r.h; if keybind_button(r, "Viewer", .LEVEL_TOOL_VIEWER, *t_button_selectable(theme, current_tool_mode == .VIEWER)) then current_tool_mode = .VIEWER; r.y += r.h; @@ -370,7 +388,7 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) { GR.label(r, "Click start of line", *t_label_left(theme)); } r.y += r.h; - } else if current_tool_mode == .INSPECTOR { + } else if current_tool_mode == .INSPECTOR || current_tool_mode == .ENTITY { r.h = ui_h(3, 2); if GR.button(r, ifx editor_billboards_visible then "Hide Markers" else "Show Markers", *theme.button_theme, 199) { editor_billboards_visible = !editor_billboards_visible; @@ -510,6 +528,121 @@ remove_trile :: (x: s32, y: s32, z: s32) { +level_editor_clear_entity_selection :: () { + entity_selected_id = -1; + entity_gizmo.hover = .NONE; + entity_gizmo.active = .NONE; +} + +get_selected_entity :: (world: *World) -> *Entity { + if entity_selected_id < 0 then return null; + for e: world.entities if cast(s64)e.id == entity_selected_id return e; + return null; +} + +// World-space box covering an entity's cell plus every part it draws, used for +// click-picking and for the selection outline. +entity_bounds :: (e: *Entity) -> (mn: Vector3, mx: Vector3) { + mn := e.position; + mx := e.position + Vector3.{1, 1, 1}; + for part: get_entity_parts(e.type) { + p, q : Vector3 = ---; + if part.kind == .TRILE { + p = entity_trile_point(e, part.offset); + q = p + Vector3.{1, 1, 1}; + } else { + // Billboards and emitters are points; give them enough of a box to click. + c := entity_point(e, part.offset); + p = c - Vector3.{0.25, 0.25, 0.25}; + q = c + Vector3.{0.25, 0.25, 0.25}; + } + mn.x = min(mn.x, p.x); mn.y = min(mn.y, p.y); mn.z = min(mn.z, p.z); + mx.x = max(mx.x, q.x); mx.y = max(mx.y, q.y); mx.z = max(mx.z, q.z); + } + return mn, mx; +} + +// The billboard that stands in for an entity in the editor: the type's declared +// EDITOR_MARKER, or a default one when the entity draws nothing you could aim +// at. Null for entities that are already visible on their own. +entity_marker_animation :: (e: *Entity) -> *Animation { + marker := get_entity_editor_marker(e.type); + if marker.count > 0 return get_animation_from_string(marker); + for part: get_entity_parts(e.type) { + if part.kind == .TRILE || part.kind == .BILLBOARD return null; + } + return get_animation_from_string("game_core.ball"); +} + +pick_entity :: (world: *World, ray: Ray) -> *Entity { + best : *Entity = null; + best_dist : float = FLOAT32_MAX; + for e: world.entities { + mn, mx := entity_bounds(e); + col := does_ray_hit_cube(ray, .{position = mn, size = mx - mn}); + if !col.hit then continue; + d := abs(col.distance); + if d < best_dist { + best_dist = d; + best = e; + } + } + return best; +} + +snap_to_step :: (v: float, step: float) -> float { + if step <= 0 then return v; + return floor(v / step + 0.5) * step; +} + +tick_entity_tool :: (ray: Ray) { + curworld := get_current_world(); + if !curworld.valid then return; + world := *curworld.world; + + e := get_selected_entity(world); + if e == null { + entity_gizmo.hover = .NONE; + entity_gizmo.active = .NONE; + } else { + mn, mx := entity_bounds(e); + debug_aabb_3d_overlay(mn, mx, .{1.0, 0.75, 0.1, 0.8}, 2.0); + + // The gizmo sits at the middle of the entity's cell, which is both what + // the entity's marker draws on and what it rotates about. + cam := get_level_editor_camera(); + origin := e.position + ENTITY_PIVOT; + if entity_gizmo_mode == .MOVE { + moved, changed := gizmo_translate(*entity_gizmo, origin, cam, ray); + if changed { + p := moved - ENTITY_PIVOT; + if entity_snap_enabled { + p.x = snap_to_step(p.x, entity_snap_step); + p.y = snap_to_step(p.y, entity_snap_step); + p.z = snap_to_step(p.z, entity_snap_step); + } + e.position = p; + } + } else { + u, v, n, steps := gizmo_rotate(*entity_gizmo, origin, cam, ray); + if steps != 0 { + // A negative drag is the same turn taken the other way round. + turn := ifx steps > 0 then orientation_quarter_turn(u, v, n) else orientation_quarter_turn(v, u, n); + for 1..abs(steps) e.orientation = compose_orientations(turn, e.orientation); + } + } + for *inst: e.emitters inst.position = entity_point(e, inst.offset); + } + + // A click that didn't land on the gizmo selects (or clears) the selection. + if entity_gizmo.active == .NONE && entity_gizmo.hover == .NONE { + if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .START { + picked := pick_entity(world, ray); + entity_selected_id = ifx picked then cast(s64)picked.id else cast(s64)-1; + } + } +} + editor_edit_y :: () -> float { y := cast(float)editY; curworld := get_current_world(); @@ -525,6 +658,9 @@ tick_level_editor :: () { tick_level_editor_camera(); tick_particles(cast(float)delta_time); + curworld := get_current_world(); + if curworld.valid then tick_entity_emitters(*curworld.world, cast(float)delta_time); + if is_action_start(Editor_Action.LEVEL_TWIST_CCW) { lastInputTime = get_time(); current_orientation_twist = (current_orientation_twist + 1) % 4; @@ -538,9 +674,14 @@ tick_level_editor :: () { current_orientation_face = (current_orientation_face + 1) % 6; } - ray := get_mouse_ray(*get_level_editor_camera()); + cam := get_level_editor_camera(); + ray := get_mouse_ray(*cam); hit, point := ray_plane_collision_point(ray, editor_edit_y(), 20); + // The entity tool works off the entities themselves, not the edit plane, so + // it runs whether or not the cursor is over the plane. + if current_tool_mode == .ENTITY then tick_entity_tool(ray); + show_trile_preview = false; if hit { show_trile_preview = true; @@ -632,6 +773,12 @@ tick_level_editor :: () { inspector_z = cast(s32)pz; inspector_note_deactivate = true; } + } else if current_tool_mode == .ENTITY { + // Right-click places the type picked in the panel, so the cursor never + // has to leave the spot you're aiming at. + if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .START { + add_entity_at(cast(s32)px, cast(s32)py, cast(s32)pz); + } } } } @@ -642,6 +789,9 @@ create_level_editor_preview_tasks :: () { px := trile_preview_x; py := trile_preview_y; pz := trile_preview_z; + // The entity tool never paints triles, so it shows no brush preview. + if current_tool_mode == .ENTITY then return; + if current_tool_mode == .INSPECTOR { name, orientation, found := get_trile_at(*curworld.world, xx px, xx py, xx pz); cursor_trile := ifx found then name else (ifx editor_current_trile then editor_current_trile.name else ""); @@ -980,11 +1130,131 @@ draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) { } } +draw_entity_tool_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) { + curworld := get_current_world(); + if !curworld.valid then return; + world := *curworld.world; + + r.h = ui_h(3, 2); + GR.label(r.*, "-- Entity Tool --", *t_label_left(theme)); + r.y += r.h; + + e := get_selected_entity(world); + if e != null { + GR.label(r.*, tprint("Type: %", entity_type_name(e.type)), *t_label_left(theme)); + r.y += r.h; + GR.label(r.*, tprint("Pos: %, %, %", + formatFloat(e.position.x, trailing_width=2), + formatFloat(e.position.y, trailing_width=2), + formatFloat(e.position.z, trailing_width=2)), *t_label_left(theme)); + r.y += r.h; + + GR.label(r.*, tprint("Orientation: %", e.orientation), *t_label_left(theme)); + r.y += r.h; + + r.h = ui_h(4, 0); + mode_r := r.*; + mode_r.w = r.w / 2; + if GR.button(mode_r, "Move", *t_button_selectable(theme, entity_gizmo_mode == .MOVE), 524) { + entity_gizmo_mode = .MOVE; + entity_gizmo.active = .NONE; + } + mode_r.x += mode_r.w; + if GR.button(mode_r, "Rotate", *t_button_selectable(theme, entity_gizmo_mode == .ROTATE), 525) { + entity_gizmo_mode = .ROTATE; + entity_gizmo.active = .NONE; + } + r.y += r.h; + + // Rotation is always in quarter turns, so snapping is a move-only setting. + if entity_gizmo_mode == .MOVE { + snap_label := ifx entity_snap_enabled then tprint("Snap: %", formatFloat(entity_snap_step, trailing_width=2)) else "Snap: off"; + if GR.button(r.*, snap_label, *t_button_selectable(theme, entity_snap_enabled), 520) { + entity_snap_enabled = !entity_snap_enabled; + } + r.y += r.h; + if entity_snap_enabled { + GR.slider(r.*, *entity_snap_step, 0.125, 1.0, 0.125, *theme.slider_theme); + r.y += r.h; + } + } + + fields_r := r.*; + fields_r.h = ui_h(30, 0); + entity_autoedit(fields_r, e, theme); + r.y += fields_r.h; + + if GR.button(r.*, "Remove Entity", *theme.button_theme, 521) { + remove_entity(world, e); + level_editor_clear_entity_selection(); + } + r.y += r.h; + if GR.button(r.*, "Deselect", *theme.button_theme, 522) { + level_editor_clear_entity_selection(); + } + r.y += r.h; + } else { + GR.label(r.*, "Click an entity to select it.", *t_label_left(theme)); + r.y += r.h; + GR.label(r.*, "Drag the gizmo to move or turn.", *t_label_left(theme)); + r.y += r.h; + } + + r.h = ui_h(3, 2); + r.y += r.h * 0.5; + GR.label(r.*, "-- Spawn --", *t_label_left(theme)); + r.y += r.h; + + r.h = ui_h(4, 0); + for info, idx: ENTITY_TYPE_TABLE { + selected := cast(s32)idx == entity_add_type; + if GR.button(r.*, info.name, *t_button_selectable(theme, selected), cast(s32)(500 + idx)) { + entity_add_type = cast(s32)idx; + } + r.y += r.h; + } + r.h = ui_h(3, 2); + if entity_add_type >= 0 && entity_add_type < cast(s32)ENTITY_TYPE_TABLE.count { + GR.label(r.*, "Right-click in the world to place.", *t_label_left(theme)); + } else { + GR.label(r.*, "Pick a type to place.", *t_label_left(theme)); + } + r.y += r.h; +} + +// Places the type picked in the panel. Called from the right-click handler, so +// the position is whatever cell the cursor is over on the edit plane. +add_entity_at :: (x: s32, y: s32, z: s32) { + if entity_add_type < 0 || entity_add_type >= cast(s32)ENTITY_TYPE_TABLE.count then return; + curworld := get_current_world(); + if !curworld.valid then return; + spawned := spawn_entity(*curworld.world, ENTITY_TYPE_TABLE[entity_add_type].name, .{cast(float)x, cast(float)y, cast(float)z}); + if spawned then entity_selected_id = cast(s64)spawned.id; +} + +// Runs autoedit on the entity's concrete type, so each type gets an editor +// generated from its own fields. +entity_autoedit :: (rect: GR.Rect, e: *Entity, theme: *GR.Overall_Theme) { + #insert #run,stallable gen_entity_dispatch("autoedit(rect, cast(*%)e, theme, 400)"); +} + add_editor_billboards :: () { if !editor_billboards_visible then return; curworld := get_current_world(); if !curworld.valid then return; + // Entities are marked first, before the fallback animation is required, so a + // type with its own EDITOR_MARKER still shows up if "game_core.ball" is gone. + for e: curworld.world.entities { + marker := entity_marker_animation(e); + if marker == null then continue; + task : Rendering_Task_Billboard; + task.position = e.position + ENTITY_PIVOT; + task.animation = marker; + task.frame = 0; + add_rendering_task(task); + } + anim := get_animation_from_string("game_core.ball"); if anim == null then return; @@ -1012,6 +1282,7 @@ draw_level_editor :: () { ph := effective_plane_height(*curworld.world.conf); create_set_cam_rendering_task(cam, ph); create_world_rendering_tasks(*curworld.world, cam, ph); + add_entity_render_tasks(*curworld.world); if show_trile_preview && !trile_preview_disabled { create_level_editor_preview_tasks(); } @@ -1043,12 +1314,14 @@ draw_level_editor_ui :: (theme: *GR.Overall_Theme) { case .INFO; if curworld.valid then autoedit(r, *curworld.world.conf, theme); } - if current_tool_mode == .INSPECTOR { + if current_tool_mode == .INSPECTOR || current_tool_mode == .ENTITY { rr := GR.get_rect(ui_w(85,0), ui_h(5,0), ui_w(15, 0), ui_h(95, 0)); draw_bg_rectangle(rr, theme); ui_add_mouse_occluder(rr); rr.y += ui_h(1, 0); - if inspector_selected { + if current_tool_mode == .ENTITY { + draw_entity_tool_panel(*rr, theme); + } else if inspector_selected { draw_inspector_panel(*rr, theme); } else { rr.h = ui_h(3, 2); diff --git a/src/entities.jai b/src/entities.jai new file mode 100644 index 0000000..006e3b6 --- /dev/null +++ b/src/entities.jai @@ -0,0 +1,408 @@ +// Entity types are plain structs declared in game code, registered with an +// @Entity note the way console commands register with @Command. The metaprogram +// (src/meta/entity_types.jai) collects them into ENTITY_TYPES, and spawning, +// dispatch, editor UI and saving all come from that. Marking a struct @Entity +// without embedding Entity, or embedding Entity without the note, is an error. +// +// Foo :: struct { +// #as using base : Entity; +// base.type = Foo; +// PARTS :: Entity_Part.[...]; // visuals, may be empty +// EDITOR_MARKER :: "pack.animation"; // optional, editor-only billboard +// } @Entity +// entity_tick :: (using foo: *Foo, dt: float) {} // optional +// entity_draw :: (foo: *Foo) {} // optional +// +// Fields of type float, s32, bool, string or Vector3 are editable and saved; +// @Slider / @Color notes pick the editor widget. +// +// Each frame the game calls tick_entities, then tick_entity_emitters, then +// add_entity_render_tasks. The level editor calls the last two only: it shows +// entities and their particles without running their game logic. + +#scope_export + +Entity :: struct { + type : Type; + id : u32; + position : Vector3; + orientation : u8; // 0..23, see orientation.jai + emitters : [..]Particle_Emitter_Instance; // built from PARTS, not saved +} + +Entity_Part_Kind :: enum { + TRILE; + BILLBOARD; + EMITTER; +} + +Entity_Part :: struct { + kind : Entity_Part_Kind; + trile : string; // TRILE + orientation : u8; // TRILE, 0..23 + animation : string; // BILLBOARD, e.g. "game_core.ball" + emitter : string; // EMITTER, a particle definition name + offset : Vector3; +} + +Entity_Type_Info :: struct { + name : string; + type : Type; + parts : []Entity_Part; + editor_marker : string; + create : () -> *Entity; +} + +#insert #run,stallable gen_entity_type_table(); + +get_entity_type_info :: (type: Type) -> *Entity_Type_Info { + for * ENTITY_TYPE_TABLE if it.type == type return it; + return null; +} + +get_entity_parts :: (type: Type) -> []Entity_Part { + info := get_entity_type_info(type); + return ifx info then info.parts else .[]; +} + +get_entity_editor_marker :: (type: Type) -> string { + info := get_entity_type_info(type); + return ifx info then info.editor_marker else ""; +} + +entity_type_name :: (type: Type) -> string { + return (cast(*Type_Info_Struct) type).name; +} + +// Entities turn about the middle of their own cell, so a rotated entity stays +// on the block it was placed on. +ENTITY_PIVOT :: Vector3.{0.5, 0.5, 0.5}; + +entity_point :: (e: *Entity, offset: Vector3) -> Vector3 { + return e.position + ENTITY_PIVOT + rotate_by_orientation(e.orientation, offset - ENTITY_PIVOT); +} + +// A trile part fills a cell rather than being a point, so the pivot cancels out. +entity_trile_point :: (e: *Entity, offset: Vector3) -> Vector3 { + return e.position + rotate_by_orientation(e.orientation, offset); +} + +// Orientation is taken up front rather than assigned afterwards because the +// emitters are placed relative to it. +spawn_entity :: (world: *World, type_name: string, position: Vector3, orientation: u8 = 0) -> *Entity { + for ENTITY_TYPE_TABLE { + if it.name != type_name continue; + e := it.create(); + e.position = position; + e.orientation = orientation; + e.id = world.next_entity_id; + world.next_entity_id += 1; + init_entity_emitters(e); + array_add(*world.entities, e); + return e; + } + log_error("Unknown entity type: %", type_name); + return null; +} + +get_first_entity :: (world: *World, type: Type) -> *Entity { + for e: world.entities if e.type == type return e; + return null; +} + +remove_entity :: (world: *World, e: *Entity) { + for other, i: world.entities { + if other != e continue; + array_ordered_remove_by_index(*world.entities, i); + free_entity(e); + return; + } +} + +free_entity :: (e: *Entity) { + array_free(e.emitters); + free(e); +} + +free_all_entities :: (world: *World) { + for e: world.entities free_entity(e); + array_reset(*world.entities); +} + +// Concrete per-type overloads in game code take precedence over these. +entity_tick :: (e: *$T/Entity, dt: float) {} +entity_draw :: (e: *$T/Entity) { draw_entity_parts(e); } + +tick_entities :: (world: *World, dt: float) { + for e: world.entities entity_dispatch_tick(e, dt); +} + +entity_dispatch_tick :: (e: *Entity, dt: float) { + #insert #run,stallable gen_entity_dispatch("entity_tick(cast(*%)e, dt)"); +} + +entity_dispatch_draw :: (e: *Entity) { + #insert #run,stallable gen_entity_dispatch("entity_draw(cast(*%)e)"); +} + +// Dispatches a call on the entity's concrete type. '%' in 'call' becomes the type +// name. Also used by the editor to run autoedit on an entity's own fields. +gen_entity_dispatch :: (call: string) -> string { + builder : String_Builder; + for ENTITY_TYPES { + name := (cast(*Type_Info_Struct) it).name; + print_to_builder(*builder, "if e.type == % { %; return; }\n", name, tprint(call, name)); + } + return builder_to_string(*builder); +} + +init_entity_emitters :: (e: *Entity) { + for part: get_entity_parts(e.type) { + if part.kind != .EMITTER continue; + inst : Particle_Emitter_Instance; + inst.definition_name = part.emitter; + inst.definition = get_emitter_def(part.emitter); + inst.offset = part.offset; + inst.position = entity_point(e, part.offset); + inst.active = true; + array_add(*e.emitters, inst); + } +} + +tick_entity_emitters :: (world: *World, dt: float) { + for e: world.entities { + for *inst: e.emitters { + if inst.definition == null { + inst.definition = get_emitter_def(inst.definition_name); + if inst.definition == null continue; + } + inst.position = entity_point(e, inst.offset); + tick_emitter_instance(inst, dt); + } + } +} + +// Once per frame, from the game and from the editor. Trile draws are batched per +// (trile, chunk) and flushed as TRILE_DYNAMIC tasks at the end. +add_entity_render_tasks :: (world: *World) { + array_reset_keeping_memory(*entity_trile_batches); + for e: world.entities entity_dispatch_draw(e); + for *batch: entity_trile_batches { + task : Rendering_Task_Trile_Dynamic; + task.trile = batch.trile; + task.positions = batch.positions; + task.chunk_key = batch.chunk_key; + task.worldConf = *world.conf; + add_rendering_task(task); + } +} + +entity_draw_trile :: (e: *Entity, trile: string, offset: Vector3, orientation: u8 = 0) { + pos := entity_trile_point(e, offset); + ori := compose_orientations(e.orientation, orientation); + key := world_to_chunk_coord(cast(s32) floor(pos.x), cast(s32) floor(pos.y), cast(s32) floor(pos.z)); + instance := Vector4.{pos.x, pos.y, pos.z, cast(float) ori}; + + for *batch: entity_trile_batches { + if batch.trile == trile && batch.chunk_key == key { + array_add(*batch.positions, instance); + return; + } + } + batch : Entity_Trile_Batch; + batch.trile = trile; + batch.chunk_key = key; + batch.positions.allocator = temp; + array_add(*batch.positions, instance); + array_add(*entity_trile_batches, batch); +} + +entity_draw_billboard :: (e: *Entity, animation: string, offset: Vector3, frame: s32 = 0) { + anim := get_animation_from_string(animation); + if anim == null return; + task : Rendering_Task_Billboard; + task.position = entity_point(e, offset); + task.animation = anim; + task.frame = frame; + add_rendering_task(task); +} + +draw_entity_parts :: (e: *Entity) { + for part: get_entity_parts(e.type) { + if part.kind == { + case .TRILE; entity_draw_trile(e, part.trile, part.offset, part.orientation); + case .BILLBOARD; entity_draw_billboard(e, part.animation, part.offset); + case .EMITTER; // tick_entity_emitters handles these + } + } +} + +// Fields are saved as name/value string pairs rather than typed JSON, so adding, +// removing or renaming one never breaks loading an older world. +Entity_Field :: struct { + name : string; + value : string; +} + +entity_fields_to_strings :: (e: *Entity) -> []Entity_Field { + result : [..]Entity_Field; + result.allocator = temp; + for member: entity_saved_members(e.type) { + ptr := (cast(*u8) e) + member.offset_in_bytes; + value : string; + if member.type == type_info(float) value = tprint("%", (cast(*float) ptr).*); + else if member.type == type_info(s32) value = tprint("%", (cast(*s32) ptr).*); + else if member.type == type_info(bool) value = tprint("%", (cast(*bool) ptr).*); + else if member.type == type_info(string) value = (cast(*string) ptr).*; + else if member.type == type_info(Vector3) { + v := (cast(*Vector3) ptr).*; + value = tprint("% % %", v.x, v.y, v.z); + } else continue; + array_add(*result, .{member.name, value}); + } + return result; +} + +entity_apply_field :: (e: *Entity, name: string, value: string) { + for member: entity_saved_members(e.type) { + if member.name != name continue; + ptr := (cast(*u8) e) + member.offset_in_bytes; + if member.type == type_info(float) { + v, ok := string_to_float(value); + if ok { (cast(*float) ptr).* = v; } + } else if member.type == type_info(s32) { + v, ok := string_to_int(value); + if ok { (cast(*s32) ptr).* = cast(s32) v; } + } else if member.type == type_info(bool) { + (cast(*bool) ptr).* = (value == "true"); + } else if member.type == type_info(string) { + (cast(*string) ptr).* = copy_string(value); + } else if member.type == type_info(Vector3) { + parts := split(value, " ",, temp); + if parts.count != 3 return; + v : Vector3; + x, okx := string_to_float(parts[0]); + y, oky := string_to_float(parts[1]); + z, okz := string_to_float(parts[2]); + if okx && oky && okz { (cast(*Vector3) ptr).* = .{x, y, z}; } + } + return; + } + log_warn("Entity type % has no field '%', skipping", entity_type_name(e.type), name); +} + +#scope_file + +#import "String"; + +// The members an entity type declares itself: constants and the Entity base are +// neither editable nor saved. +entity_saved_members :: (type: Type) -> []Type_Info_Struct_Member { + result : [..]Type_Info_Struct_Member; + result.allocator = temp; + for (cast(*Type_Info_Struct) type).members { + if it.flags & (.CONSTANT | .USING) continue; + array_add(*result, it); + } + return result; +} + +Entity_Trile_Batch :: struct { + trile : string; + chunk_key : Chunk_Key; + positions : [..]Vector4; +} + +entity_trile_batches : [..]Entity_Trile_Batch; + +#if FLAG_TEST_ENGINE { + // The two placement helpers use different pivots, and getting them the wrong + // way round only shows up as parts drifting off a rotated entity, so both are + // pinned to the invariant they exist for. + test_entity_part_placement :: () { + s := begin_suite("entity part placement"); + + e : Entity; + e.position = .{10, 3, -5}; + + // A trile part on the entity's own cell has to stay on that cell however + // the entity is turned, or rotating would walk it off the block it was + // placed on. + on_its_block := true; + for ori: 0..ORIENTATION_COUNT-1 { + e.orientation = cast(u8) ori; + if entity_trile_point(*e, .{0, 0, 0}) != e.position on_its_block = false; + } + check(*s, "a trile part on the entity's own cell never moves", on_its_block); + + // Point parts turn about the middle of that same cell, so a part sitting + // at the middle is the fixed point of all 24 rotations. + at_the_middle := true; + for ori: 0..ORIENTATION_COUNT-1 { + e.orientation = cast(u8) ori; + if entity_point(*e, ENTITY_PIVOT) != e.position + ENTITY_PIVOT at_the_middle = false; + } + check(*s, "a point part at the middle never moves", at_the_middle); + + e.orientation = 0; + check(*s, "unturned point parts sit at their authored offset", + entity_point(*e, .{1, 2, 3}) == e.position + Vector3.{1, 2, 3}); + check(*s, "unturned trile parts sit at their authored offset", + entity_trile_point(*e, .{1, 2, 3}) == e.position + Vector3.{1, 2, 3}); + + // Orientation 1 is the quarter twist about Y, which sends X to Z. + e.orientation = 1; + check(*s, "a turned entity carries its parts around with it", + entity_trile_point(*e, .{1, 0, 0}) == e.position + Vector3.{0, 0, 1}); + + end_suite(s); + } + + // Every declared type must reach the table, or it would be unspawnable and + // unsaveable with no diagnostic anywhere. + test_entity_type_table :: () { + s := begin_suite("entity type table"); + check(*s, "the table covers every declared type", ENTITY_TYPE_TABLE.count == ENTITY_TYPES.count); + + found_all := true; + for type: ENTITY_TYPES if get_entity_type_info(type) == null found_all = false; + check(*s, "every declared type is findable by type", found_all); + + names_match := true; + for info: ENTITY_TYPE_TABLE if info.name != entity_type_name(info.type) names_match = false; + check(*s, "table names match the type names saves use", names_match); + + // Entity itself is never a declared type, so it stands in for one that + // this build does not have. + check(*s, "an unknown type has no info", get_entity_type_info(Entity) == null); + check(*s, "an unknown type has no parts", get_entity_parts(Entity).count == 0); + end_suite(s); + } + + // Stallable because the type table is: ENTITY_TYPES only exists once the + // metaprogram has seen every @Entity in the program. + #run,stallable { + test_entity_part_placement(); + test_entity_type_table(); + } +} + +gen_entity_type_table :: () -> string { + builder : String_Builder; + append(*builder, "ENTITY_TYPE_TABLE :: Entity_Type_Info.[\n"); + for ENTITY_TYPES { + ti := cast(*Type_Info_Struct) it; + marker := "\"\""; + for member: ti.members { + if member.name == "EDITOR_MARKER" && (member.flags & .CONSTANT) { + marker = tprint("%.EDITOR_MARKER", ti.name); + break; + } + } + print_to_builder(*builder, + " .{name = \"%\", type = %, parts = %.PARTS, editor_marker = %, create = () -> *Entity { return New(%); }},\n", + ti.name, ti.name, ti.name, marker, ti.name); + } + append(*builder, "];\n"); + return builder_to_string(*builder); +} diff --git a/src/input/keybinds.jai b/src/input/keybinds.jai index 2061043..3d682ac 100644 --- a/src/input/keybinds.jai +++ b/src/input/keybinds.jai @@ -28,6 +28,7 @@ Editor_Action :: enum { LEVEL_TOOL_LINE; LEVEL_TOOL_INSPECTOR; LEVEL_TOOL_VIEWER; + LEVEL_TOOL_ENTITY; // Level editor — tabs LEVEL_TAB_TOOLS; LEVEL_TAB_INFO; @@ -180,6 +181,7 @@ set_default_bindings :: () { set(.LEVEL_TOOL_LINE, cast(Key_Code) #char "4"); set(.LEVEL_TOOL_INSPECTOR, cast(Key_Code) #char "5"); set(.LEVEL_TOOL_VIEWER, cast(Key_Code) #char "6"); + set(.LEVEL_TOOL_ENTITY, cast(Key_Code) #char "7"); set(.LEVEL_TAB_TOOLS, cast(Key_Code) #char "T"); set(.LEVEL_TAB_INFO, cast(Key_Code) #char "I"); set(.LEVEL_TAB_TACOMA, cast(Key_Code) #char "X"); diff --git a/src/load.jai b/src/load.jai index b15c950..df6b7a7 100644 --- a/src/load.jai +++ b/src/load.jai @@ -1,4 +1,4 @@ -#if FLAG_TEST_EXE_ENGINE { +#if FLAG_USE_TEST_GAME { PACK_DIR :: "./test_packs"; GAME_RESOURCES_DIR :: "./test_game/resources"; } else { diff --git a/src/logging.jai b/src/logging.jai index f5178c8..1bcff01 100644 --- a/src/logging.jai +++ b/src/logging.jai @@ -18,8 +18,10 @@ _emit :: (level: Log_Level, message: string) { else "[INFO] "; // We need to protect the logger from custom allocators since we don't - // want those meddling with our log row allocation! - push_allocator(default_allocator); + // want those meddling with our log row allocation! Before the engine has + // set one up — at compile time, in a #run — there is nothing to protect + // against and nothing to allocate from, so the current one has to do. + push_allocator(ifx default_allocator.proc then default_allocator else context.allocator); line := copy_string(tprint("%1%2", prefix, message)); print("%\n", line); console_add_output_line(line); diff --git a/src/main.jai b/src/main.jai index e801ce6..52aaff8 100644 --- a/src/main.jai +++ b/src/main.jai @@ -15,6 +15,17 @@ String :: #import "String"; Jaison :: #import "Jaison"; stbi :: #import "stb_image"; +FLAG_ANY_TEST :: FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE || FLAG_TEST_GAME || FLAG_TEST_EXE_GAME; +FLAG_ANY_EXE_TEST :: FLAG_TEST_EXE_ENGINE || FLAG_TEST_EXE_GAME; + +// The engine's own tests build against test_game/, so they still compile on a +// checkout that does not have the game/ directory. Game tests build against the +// real game, which is the whole point of them. +FLAG_USE_TEST_GAME :: FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE; + +// The framework (suites, checks, the exe-test command list) is available under +// every test flag; the engine's own test cases are not. +#if FLAG_ANY_TEST { #load "tests/framework.jai"; } #if (FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE) { #load "tests/index.jai"; } #load "logging.jai"; @@ -32,6 +43,8 @@ stbi :: #import "stb_image"; #load "ray.jai"; #load "profiling.jai"; #load "particles/particles.jai"; +#load "orientation.jai"; +#load "entities.jai"; #load "world.jai"; #load "utils.jai"; #load "audio/audio.jai"; @@ -40,10 +53,10 @@ stbi :: #import "stb_image"; #load "loading_screen.jai"; #load "ui/demo.jai"; -#if !FLAG_TEST_EXE_ENGINE { - #load "../game/game.jai"; -} else { +#if FLAG_USE_TEST_GAME { #load "../test_game/game.jai"; +} else { + #load "../game/game.jai"; } last_frame_time : float64; // timestamp of the last frame @@ -139,6 +152,10 @@ init :: () { #if FLAG_TEST_EXE_ENGINE { engine_exe_tests_add(); } + #if FLAG_TEST_EXE_GAME { + // Fixed-name hook, same convention as game_init / game_tick. + game_exe_tests_add(); + } } init_after_core :: () { @@ -225,7 +242,7 @@ frame :: () { add_frame_profiling_point("After loading logic"); - #if FLAG_TEST_EXE_ENGINE { + #if FLAG_ANY_EXE_TEST { run_exe_tests(); } diff --git a/src/meta/entity_types.jai b/src/meta/entity_types.jai new file mode 100644 index 0000000..382201e --- /dev/null +++ b/src/meta/entity_types.jai @@ -0,0 +1,98 @@ +/* + Entity types register themselves with an @Entity note on the struct, the same + way console commands register with @Command. The generators in entities.jai + all read one list, ENTITY_TYPES, which is built here and injected once the + rest of the program has been typechecked — that is why those generators are + written as #run,stallable. +*/ + +#import "Sort"; + +entity_type_names : [..]string; +entity_types_emitted := false; + +add_entity_types :: (message: *Message, w: *Workspace) { + if message.kind == .PHASE { + phase := cast(*Message_Phase) message; + if phase.phase == .TYPECHECKED_ALL_WE_CAN && !entity_types_emitted { + entity_types_emitted = true; + emit_entity_types(w); + } + return; + } + + if message.kind != .TYPECHECKED then return; + code := cast(*Message_Typechecked) message; + + for code.declarations { + decl := it.expression; + if !decl.expression || decl.expression.kind != .STRUCT then continue; + if !is_main_program(decl) then continue; + + noted := false; + for note: decl.notes if note.text == "Entity" noted = true; + embeds := struct_embeds_entity(cast(*Code_Struct) decl.expression); + + // Without this the note is as easy to forget as the hand-written list it + // replaced, and forgetting it is silent: the type compiles fine and is + // simply invisible to spawning, the editor and saving. + if embeds && !noted { + report(decl, sprint("Struct % embeds Entity but is not marked @Entity, so nothing can spawn, edit or save it.", decl.name)); + continue; + } + if !noted then continue; + + if !embeds { + report(decl, sprint("Struct % is marked @Entity but has no '#as using base : Entity;'.", decl.name)); + continue; + } + if decl.flags & .SCOPE_FILE { + report(decl, sprint("Entity type % is file scoped, that is not okay. The generated type table has to name it.", decl.name)); + continue; + } + + array_add(*entity_type_names, sprint("%", decl.name)); + } +} + +#scope_file + +// Emitted even when the game declares no entity types at all: the generators in +// entities.jai are stalled waiting for this name and would never come unstuck. +emit_entity_types :: (w: *Workspace) { + // Sorted by name so the generated table — and with it the order of the + // editor's spawn list — does not shuffle between builds just because + // typechecking happened to finish in a different order. + quick_sort(entity_type_names, compare_strings); + + builder : String_Builder; + append(*builder, "ENTITY_TYPES :: Type.["); + for entity_type_names { + if it_index > 0 then append(*builder, ", "); + append(*builder, it); + } + append(*builder, "];\n"); + add_build_string(builder_to_string(*builder), w.*); +} + +struct_embeds_entity :: (s: *Code_Struct) -> bool { + if !s.defined_type then return false; + for s.defined_type.members { + if !(it.flags & .USING) then continue; + if it.type.type != .STRUCT then continue; + if (cast(*Type_Info_Struct) it.type).name == "Entity" then return true; + } + return false; +} + +// Game and engine code only. Module code never declares entity types, and +// build strings have no enclosing load to ask about. +is_main_program :: (decl: *Code_Declaration) -> bool { + if !decl.enclosing_load then return false; + if !decl.enclosing_load.enclosing_import then return false; + return decl.enclosing_load.enclosing_import.module_type == .MAIN_PROGRAM; +} + +report :: (decl: *Code_Declaration, error: string) { + compiler_report(error, make_location(cast(*Code_Node) decl), .ERROR); +} diff --git a/src/meta/meta.jai b/src/meta/meta.jai index fc74a41..aeebb46 100644 --- a/src/meta/meta.jai +++ b/src/meta/meta.jai @@ -1,8 +1,10 @@ #load "ascii.jai"; +#load "../resource_mirror.jai"; #load "pack.jai"; #load "lint.jai"; #load "hacks.jai"; #load "console_commands.jai"; +#load "entity_types.jai"; #load "shaderload.jai"; endframe_modified := false; @@ -14,4 +16,6 @@ custom_message_handler :: (message: *Message, w: *Workspace) { remove_getrect_custom_cursor(message, w); add_console_commands(message, w); + + add_entity_types(message, w); } diff --git a/src/meta/pack.jai b/src/meta/pack.jai index 6930910..bf8fd53 100644 --- a/src/meta/pack.jai +++ b/src/meta/pack.jai @@ -3,11 +3,9 @@ should_ignore_file :: (name: string) -> bool { if name.count > 0 && name[0] == #char "." return true; - ok, left, right := split_from_right(name, #char "."); - if right == "aseprite" { - print("Ignoring % as aseprite file...\n", name); - return true; - } + // Aseprite sources are inputs (see resource_mirror.jai), not shippable + // assets; only the .sheet.png/.sheet.json they export go into the pack. + if is_aseprite_source(name) return true; return false; } @@ -53,11 +51,10 @@ create_pack :: (include_test_resources: bool = false, pack_dir: string = "./pack #import "Simple_Package"; #import "File"; - util.visit_files("./resources", true, true, file_visit_handler); - if include_test_resources { - util.visit_files("./test_game/resources", true, true, file_visit_handler); - } else { - util.visit_files("./game/resources", true, true, file_visit_handler); + // Packs come from the processed copy of the resource trees, never the trees + // themselves. See resource_mirror.jai. + for prepare_resource_mirror(include_test_resources) { + util.visit_files(it, true, true, file_visit_handler); } make_directory_if_it_does_not_exist(pack_dir); diff --git a/src/orientation.jai b/src/orientation.jai new file mode 100644 index 0000000..e71437e --- /dev/null +++ b/src/orientation.jai @@ -0,0 +1,291 @@ +// The 24 axis-aligned cube rotations, encoded as 'face * 4 + twist' exactly as +// the trile shaders decode them (see get_orientation_matrix in +// shader_trile_shadow.glsl). Everything here mirrors that decode, so a rotation +// applied to a position on the CPU lines up with the geometry the GPU draws. +// +// Note the shaders build their mat3s from columns, which makes each of their +// rot_* helpers the transpose of the usual form. The matrices below are the +// effective row-major operators (v' = M * v), so they match what the shader +// actually does rather than what its helper names suggest. + +ORIENTATION_COUNT :: 24; + +Orientation_Matrix :: struct { + m : [3][3]s8; +} + +orientation_matrix :: (ori: u8) -> Orientation_Matrix { + return ORIENTATION_MATRICES[ori % ORIENTATION_COUNT]; +} + +rotate_by_orientation :: (ori: u8, v: Vector3) -> Vector3 { + m := orientation_matrix(ori).m; + return .{ + cast(float)m[0][0] * v.x + cast(float)m[0][1] * v.y + cast(float)m[0][2] * v.z, + cast(float)m[1][0] * v.x + cast(float)m[1][1] * v.y + cast(float)m[1][2] * v.z, + cast(float)m[2][0] * v.x + cast(float)m[2][1] * v.y + cast(float)m[2][2] * v.z, + }; +} + +// The orientation equal to applying 'inner' first and then 'outer'. Used to fold +// an entity's rotation into the orientation its parts were authored with. +compose_orientations :: (outer: u8, inner: u8) -> u8 { + return ORIENTATION_COMPOSE[outer % ORIENTATION_COUNT][inner % ORIENTATION_COUNT]; +} + +// The quarter turn that carries 'u' onto 'v' and leaves 'n' alone, where u, v, n +// are a set of perpendicular unit axes. Taking the basis from the caller keeps +// the result consistent with whatever handedness the caller draws with, instead +// of relying on a sign convention agreed at a distance. +orientation_quarter_turn :: (u: Vector3, v: Vector3, n: Vector3) -> u8 { + // M = v*uT - u*vT + n*nT sends u -> v, v -> -u, n -> n. + m : Orientation_Matrix; + ui := to_axis_ints(u); + vi := to_axis_ints(v); + ni := to_axis_ints(n); + for i: 0..2 { + for j: 0..2 { + m.m[i][j] = vi[i]*ui[j] - ui[i]*vi[j] + ni[i]*ni[j]; + } + } + ori, ok := orientation_from_matrix(m); + if !ok { + log_error("Rotate gizmo basis is not axis-aligned; ignoring the turn."); + return 0; + } + return ori; +} + +orientation_from_matrix :: (m: Orientation_Matrix) -> (ori: u8, ok: bool) { + for i: 0..ORIENTATION_COUNT-1 { + if mat3_equal(m, ORIENTATION_MATRICES[i]) return cast(u8) i, true; + } + return 0, false; +} + +#scope_file + +ORIENTATION_MATRICES :: #run generate_orientation_matrices(); +ORIENTATION_COMPOSE :: #run generate_orientation_compose_table(); + +// Axis vectors only ever hold 0 or +-1 here, but they arrive as floats. +to_axis_ints :: (v: Vector3) -> [3]s8 { + round_axis :: (f: float) -> s8 { + if f > 0.5 then return 1; + if f < -0.5 then return -1; + return 0; + } + return .[round_axis(v.x), round_axis(v.y), round_axis(v.z)]; +} + +mat3_of :: (a: s8, b: s8, c: s8, d: s8, e: s8, f: s8, g: s8, h: s8, i: s8) -> Orientation_Matrix { + r : Orientation_Matrix; + r.m[0][0] = a; r.m[0][1] = b; r.m[0][2] = c; + r.m[1][0] = d; r.m[1][1] = e; r.m[1][2] = f; + r.m[2][0] = g; r.m[2][1] = h; r.m[2][2] = i; + return r; +} + +// (c, s) are cos and sin of the angle, always in {-1, 0, 1} here. +rot_x_effective :: (s: s8, c: s8) -> Orientation_Matrix { return mat3_of(1, 0, 0, 0, c, s, 0, -s, c); } +rot_y_effective :: (s: s8, c: s8) -> Orientation_Matrix { return mat3_of(c, 0, -s, 0, 1, 0, s, 0, c); } +rot_z_effective :: (s: s8, c: s8) -> Orientation_Matrix { return mat3_of(c, s, 0, -s, c, 0, 0, 0, 1); } + +mat3_mul :: (a: Orientation_Matrix, b: Orientation_Matrix) -> Orientation_Matrix { + r : Orientation_Matrix; + for i: 0..2 { + for j: 0..2 { + sum : s8 = 0; + for k: 0..2 sum += a.m[i][k] * b.m[k][j]; + r.m[i][j] = sum; + } + } + return r; +} + +mat3_equal :: (a: Orientation_Matrix, b: Orientation_Matrix) -> bool { + for i: 0..2 for j: 0..2 if a.m[i][j] != b.m[i][j] return false; + return true; +} + +// Mirrors get_orientation_matrix: a face rotation followed by a twist about Y. +generate_orientation_matrices :: () -> [ORIENTATION_COUNT]Orientation_Matrix { + identity :: #run mat3_of(1, 0, 0, 0, 1, 0, 0, 0, 1); + // sin/cos of face and twist angles, written out to keep this exact. + faces : [6]Orientation_Matrix; + faces[0] = identity; + faces[1] = rot_x_effective( 0, -1); // PI + faces[2] = rot_z_effective(-1, 0); // -PI/2 + faces[3] = rot_z_effective( 1, 0); // PI/2 + faces[4] = rot_x_effective( 1, 0); // PI/2 + faces[5] = rot_x_effective(-1, 0); // -PI/2 + + twists : [4]Orientation_Matrix; + twists[0] = identity; + twists[1] = rot_y_effective( 1, 0); + twists[2] = rot_y_effective( 0, -1); + twists[3] = rot_y_effective(-1, 0); + + result : [ORIENTATION_COUNT]Orientation_Matrix; + for face: 0..5 { + for twist: 0..3 { + result[face * 4 + twist] = mat3_mul(faces[face], twists[twist]); + } + } + return result; +} + +generate_orientation_compose_table :: () -> [ORIENTATION_COUNT][ORIENTATION_COUNT]u8 { + mats := generate_orientation_matrices(); + table : [ORIENTATION_COUNT][ORIENTATION_COUNT]u8; + for a: 0..ORIENTATION_COUNT-1 { + for b: 0..ORIENTATION_COUNT-1 { + product := mat3_mul(mats[a], mats[b]); + found := false; + for c: 0..ORIENTATION_COUNT-1 { + if mat3_equal(product, mats[c]) { + table[a][b] = cast(u8) c; + found = true; + break; + } + } + assert(found, "Cube rotation % * % is not one of the 24 orientations", a, b); + } + } + return table; +} + + +#if FLAG_TEST_ENGINE { + v3_eq :: (a: Vector3, b: Vector3) -> bool { + return abs(a.x - b.x) < 0.001 && abs(a.y - b.y) < 0.001 && abs(a.z - b.z) < 0.001; + } + + test_orientation_set :: () { + s := begin_suite("cube orientations"); + + distinct := true; + for i: 0..ORIENTATION_COUNT-1 { + for j: i+1..ORIENTATION_COUNT-1 { + if mat3_equal(ORIENTATION_MATRICES[i], ORIENTATION_MATRICES[j]) distinct = false; + } + } + check(*s, "all 24 orientations are distinct", distinct); + + // Rotations preserve handedness, so every determinant must be +1. A -1 + // would mean the set had picked up a reflection. + proper := true; + for i: 0..ORIENTATION_COUNT-1 { + m := ORIENTATION_MATRICES[i].m; + det := cast(int)m[0][0] * (cast(int)m[1][1]*m[2][2] - cast(int)m[1][2]*m[2][1]) + - cast(int)m[0][1] * (cast(int)m[1][0]*m[2][2] - cast(int)m[1][2]*m[2][0]) + + cast(int)m[0][2] * (cast(int)m[1][0]*m[2][1] - cast(int)m[1][1]*m[2][0]); + if det != 1 proper = false; + } + check(*s, "every orientation is a proper rotation", proper); + + check(*s, "orientation 0 is the identity", + v3_eq(rotate_by_orientation(0, .{1, 2, 3}), .{1, 2, 3})); + + // Orientations 1..3 are the twists: rotations about Y, so Y is fixed. + twists_about_y := true; + for ori: 1..3 { + if !v3_eq(rotate_by_orientation(cast(u8)ori, .{0, 1, 0}), .{0, 1, 0}) twists_about_y = false; + } + check(*s, "the twist orientations turn about Y", twists_about_y); + + // Rotations are rigid: lengths survive. + lengths_kept := true; + for ori: 0..ORIENTATION_COUNT-1 { + r := rotate_by_orientation(cast(u8)ori, .{1, 2, 3}); + if abs(length(r) - length(Vector3.{1, 2, 3})) > 0.001 lengths_kept = false; + } + check(*s, "rotating preserves length", lengths_kept); + + // Pinned against the shader's decode. If these drift, entity part offsets + // will rotate the opposite way from the trile geometry they belong to. + // Orientation 1 is face 0 twist 1, the shader's rot_y(PI/2). + check(*s, "orientation 1 (twist 90) sends X to Z", + v3_eq(rotate_by_orientation(1, .{1, 0, 0}), .{0, 0, 1})); + // Orientation 4 is face 1 twist 0, the shader's rot_x(PI). + check(*s, "orientation 4 (face 1) flips Y and Z", + v3_eq(rotate_by_orientation(4, .{0, 1, 1}), .{0, -1, -1})); + // Orientation 16 is face 4 twist 0, the shader's rot_x(PI/2). + check(*s, "orientation 16 (face 4) sends Y to -Z", + v3_eq(rotate_by_orientation(16, .{0, 1, 0}), .{0, 0, -1})); + end_suite(s); + } + + test_orientation_compose :: () { + s := begin_suite("orientation composition"); + + identity_ok := true; + for ori: 0..ORIENTATION_COUNT-1 { + if compose_orientations(0, cast(u8)ori) != cast(u8)ori identity_ok = false; + if compose_orientations(cast(u8)ori, 0) != cast(u8)ori identity_ok = false; + } + check(*s, "composing with orientation 0 changes nothing", identity_ok); + + // Composing a fixed orientation with each of the 24 must hit each exactly + // once, or the table is not a group and some rotations are unreachable. + permutation := true; + for a: 0..ORIENTATION_COUNT-1 { + seen : [ORIENTATION_COUNT]bool; + for b: 0..ORIENTATION_COUNT-1 { + r := compose_orientations(cast(u8)a, cast(u8)b); + if seen[r] permutation = false; + seen[r] = true; + } + } + check(*s, "every row of the compose table is a permutation", permutation); + + // compose(outer, inner) must mean "inner first, then outer". + order_ok := true; + v := Vector3.{1, 2, 3}; + for a: 0..ORIENTATION_COUNT-1 { + for b: 0..ORIENTATION_COUNT-1 { + combined := rotate_by_orientation(compose_orientations(cast(u8)a, cast(u8)b), v); + stepwise := rotate_by_orientation(cast(u8)a, rotate_by_orientation(cast(u8)b, v)); + if !v3_eq(combined, stepwise) order_ok = false; + } + } + check(*s, "composing matches applying inner then outer", order_ok); + end_suite(s); + } + + test_orientation_quarter_turn :: () { + s := begin_suite("orientation quarter turns"); + X :: Vector3.{1, 0, 0}; + Y :: Vector3.{0, 1, 0}; + Z :: Vector3.{0, 0, 1}; + + q := orientation_quarter_turn(X, Z, Y); // turn X toward Z about Y + check(*s, "a quarter turn carries u onto v", v3_eq(rotate_by_orientation(q, X), Z)); + check(*s, "a quarter turn carries v onto -u", v3_eq(rotate_by_orientation(q, Z), .{-1, 0, 0})); + check(*s, "a quarter turn leaves its axis alone", v3_eq(rotate_by_orientation(q, Y), Y)); + + four := compose_orientations(q, compose_orientations(q, compose_orientations(q, q))); + check(*s, "four quarter turns return to the start", four == 0); + + // Dragging the ring back the other way must undo the turn exactly. + back := orientation_quarter_turn(Z, X, Y); + check(*s, "the reverse turn cancels the forward one", compose_orientations(q, back) == 0); + + all_valid := true; + for pair: Vector3.[X, Y, Z] { + u := pair; + v := ifx v3_eq(u, X) then Y else ifx v3_eq(u, Y) then Z else X; + n := cross(u, v); + t := orientation_quarter_turn(u, v, n); + if !v3_eq(rotate_by_orientation(t, u), v) all_valid = false; + } + check(*s, "quarter turns work on every axis pair", all_valid); + end_suite(s); + } + + #run { + test_orientation_set(); + test_orientation_compose(); + test_orientation_quarter_turn(); + } +} diff --git a/src/pack_hotreload.jai b/src/pack_hotreload.jai index 2e725a1..0804a00 100644 --- a/src/pack_hotreload.jai +++ b/src/pack_hotreload.jai @@ -1,5 +1,7 @@ #if !FLAG_RELEASE_BUILD && OS != .WASM { +#load "resource_mirror.jai"; + #import "String"; Pack_Writer :: #import "Simple_Package"; File_Util :: #import "File_Utilities"; @@ -36,8 +38,11 @@ _hotreload_visitor :: (info: *File_Util.File_Visit_Info, packs: *Table(string, P recreate_packs_on_disk :: () { packs: Table(string, Pack_Writer.Create_Package); - File_Util.visit_files("./resources", true, *packs, _hotreload_visitor); - File_Util.visit_files(GAME_RESOURCES_DIR, true, *packs, _hotreload_visitor); + // Same mirror-and-process pipeline the build uses, so a hot reload picks up + // edited .aseprite files too, not just plain assets. + for prepare_resource_mirror(FLAG_USE_TEST_GAME) { + File_Util.visit_files(it, true, *packs, _hotreload_visitor); + } for pack, key: packs { Pack_Writer.write(*pack, tprint("%/%.pack", PACK_DIR, key)); log_info("Hot-reload: wrote pack '%'", key); diff --git a/src/rendering/backend.jai b/src/rendering/backend.jai index eb8c8eb..3688a9d 100644 --- a/src/rendering/backend.jai +++ b/src/rendering/backend.jai @@ -17,6 +17,7 @@ Render_Command_Type :: enum { SET_CAMERA; DRAW_GROUND; ADD_TRILE_POSITIONS; + ADD_TRILE_POSITIONS_F32; DRAW_TRILE_POSITIONS; ADD_TRILE_RDM_POSITION; DRAW_TRILE_RDM; @@ -51,6 +52,15 @@ Render_Command_Add_Trile_Positions :: struct { chunk : Chunk_Key; } +// Like Add_Trile_Positions, but takes float world-space positions directly +// (xyz = position, w = orientation 0..23). Appends into the same instance +// buffer / offset list, so draws go through Render_Command_Draw_Trile_Positions. +Render_Command_Add_Trile_Positions_F32 :: struct { + #as using c : Render_Command; + c.type = .ADD_TRILE_POSITIONS_F32; + positions : []Vector4; +} + Render_Command_Draw_Trile_Positions :: struct { #as using c : Render_Command; c.type = .DRAW_TRILE_POSITIONS; diff --git a/src/rendering/backend_sokol.jai b/src/rendering/backend_sokol.jai index 16cdd89..dfd9e95 100644 --- a/src/rendering/backend_sokol.jai +++ b/src/rendering/backend_sokol.jai @@ -22,6 +22,9 @@ backend_handle_command :: (cmd: *Render_Command) { case .ADD_TRILE_POSITIONS; add_command := cast(*Render_Command_Add_Trile_Positions)cmd; backend_add_trile_positions(add_command.positions, add_command.chunk); + case .ADD_TRILE_POSITIONS_F32; + add_f32_command := cast(*Render_Command_Add_Trile_Positions_F32)cmd; + backend_add_trile_positions_f32(add_f32_command.positions); case .DRAW_TRILE_POSITIONS; draw_command := cast(*Render_Command_Draw_Trile_Positions)cmd; backend_draw_trile_positions(draw_command.trile, draw_command.amount, draw_command.conf, draw_command.chunk_key, draw_command.preview_mode, draw_command.offset_index, draw_command.lod_index); @@ -157,6 +160,14 @@ backend_add_trile_positions :: (positions : []Trile_Instance, chunk: Chunk_Key) array_add(*trile_offsets, offset); } +backend_add_trile_positions_f32 :: (positions : []Vector4) { + offset := sg_append_buffer(gPipelines.trile.bind.vertex_buffers[3], *(sg_range.{ + ptr = positions.data, + size = size_of(Vector4) * cast(u64)positions.count, + })); + array_add(*trile_offsets, offset); +} + backend_draw_trile_positions :: (trile : string, amount : s32, worldConf: *World_Config, chunk_key: Chunk_Key, preview_mode: s32 = 0, offset_index: s32 = 0, lod_index: s32 = 0) { if in_gbuffer_pass { backend_draw_trile_positions_gbuffer(trile, amount, worldConf, offset_index, lod_index); diff --git a/src/rendering/debug_draw.jai b/src/rendering/debug_draw.jai index c3cb1e3..375a5b5 100644 --- a/src/rendering/debug_draw.jai +++ b/src/rendering/debug_draw.jai @@ -1,8 +1,26 @@ -DEBUG_LINE_MAX :: 65536; +DEBUG_LINE_MAX :: 65536; +DEBUG_OVERLAY_LINE_MAX :: 8192; -g_debug_line_verts : [DEBUG_LINE_MAX * 2 * 7]float; +// One record per line, uploaded as instance data: the line pipeline expands each +// into a screen-space quad so lines can have a real pixel width. +Debug_Line :: struct { + a : Vector3; + b : Vector3; + color : Vector4; + width : float; // in pixels +} + +DEBUG_LINE_DEFAULT_WIDTH :: 1.0; + +g_debug_lines : [DEBUG_LINE_MAX]Debug_Line; g_debug_line_count : int; +// Overlay lines ignore the depth buffer, so editor gizmos stay visible through +// terrain. They are UI rather than debug visualization, so they are not gated +// on debug_draw_enabled. +g_overlay_lines : [DEBUG_OVERLAY_LINE_MAX]Debug_Line; +g_overlay_line_count : int; + debug_draw_enabled : bool = !FLAG_RELEASE_BUILD; debug_draw_grid : bool = true; debug_draw_vectors : bool = true; @@ -24,27 +42,35 @@ toggle_debug_colliders :: () { debug_draw_colliders = !debug_draw_colliders; } @Command -debug_line :: (a: Vector3, b: Vector3, col: Vector4) { +debug_line :: (a: Vector3, b: Vector3, col: Vector4, width: float = DEBUG_LINE_DEFAULT_WIDTH) { if !debug_draw_enabled then return; if g_debug_line_count >= DEBUG_LINE_MAX then return; - base := g_debug_line_count * 14; - g_debug_line_verts[base + 0] = a.x; - g_debug_line_verts[base + 1] = a.y; - g_debug_line_verts[base + 2] = a.z; - g_debug_line_verts[base + 3] = col.x; - g_debug_line_verts[base + 4] = col.y; - g_debug_line_verts[base + 5] = col.z; - g_debug_line_verts[base + 6] = col.w; - g_debug_line_verts[base + 7] = b.x; - g_debug_line_verts[base + 8] = b.y; - g_debug_line_verts[base + 9] = b.z; - g_debug_line_verts[base + 10] = col.x; - g_debug_line_verts[base + 11] = col.y; - g_debug_line_verts[base + 12] = col.z; - g_debug_line_verts[base + 13] = col.w; + g_debug_lines[g_debug_line_count] = .{a, b, col, width}; g_debug_line_count += 1; } +debug_line_overlay :: (a: Vector3, b: Vector3, col: Vector4, width: float = DEBUG_LINE_DEFAULT_WIDTH) { + if g_overlay_line_count >= DEBUG_OVERLAY_LINE_MAX then return; + g_overlay_lines[g_overlay_line_count] = .{a, b, col, width}; + g_overlay_line_count += 1; +} + +debug_aabb_3d_overlay :: (mn: Vector3, mx: Vector3, col: Vector4, width: float = DEBUG_LINE_DEFAULT_WIDTH) { + corner :: (mn: Vector3, mx: Vector3, i: int) -> Vector3 { + return .{ ifx i & 1 then mx.x else mn.x, + ifx i & 2 then mx.y else mn.y, + ifx i & 4 then mx.z else mn.z }; + } + // Every pair of corners differing in exactly one bit is an edge of the box. + for i: 0..7 { + for bit: int.[1, 2, 4] { + j := i | bit; + if j == i then continue; + debug_line_overlay(corner(mn, mx, i), corner(mn, mx, j), col, width); + } + } +} + debug_vector :: (origin: Vector3, vec: Vector3, col: Vector4) { if !debug_draw_enabled || !debug_draw_vectors then return; tip := origin + vec; @@ -126,21 +152,34 @@ debug_aabb_3d :: (mn: Vector3, mx: Vector3, col: Vector4) { debug_line(.{mx.x, mn.y, mx.z}, .{mx.x, mx.y, mx.z}, col); } -debug_draw_flush_gpu :: () { - if g_debug_line_count == 0 then return; +flush_debug_lines :: (pipe: *Pipeline_Binding, lines: []Debug_Line, count: *int, params: *Debugline_Vs_Params) { + if count.* == 0 then return; + // Buffer 0 is the shared static quad; buffer 1 is this pipeline's instances. sg_update_buffer( - gPipelines.debugline.bind.vertex_buffers[0], + pipe.bind.vertex_buffers[1], *(sg_range.{ - ptr = g_debug_line_verts.data, - size = cast(u64)(g_debug_line_count * 14 * size_of(float)), + ptr = lines.data, + size = cast(u64)(count.* * size_of(Debug_Line)), }) ); - mvp := create_viewproj(*camera); + sg_apply_pipeline(pipe.pipeline); + sg_apply_bindings(*pipe.bind); + sg_apply_uniforms(UB_debugline_vs_params, *(sg_range.{ ptr = params, size = size_of(Debugline_Vs_Params) })); + sg_draw(0, 6, xx count.*); + count.* = 0; +} + +debug_draw_flush_gpu :: () { + if g_debug_line_count == 0 && g_overlay_line_count == 0 then return; + + w, h := get_window_size(); + mvp := create_viewproj(*camera); params : Debugline_Vs_Params; params.mvp = mvp.floats; - sg_apply_pipeline(gPipelines.debugline.pipeline); - sg_apply_bindings(*gPipelines.debugline.bind); - sg_apply_uniforms(UB_debugline_vs_params, *(sg_range.{ ptr = *params, size = size_of(Debugline_Vs_Params) })); - sg_draw(0, xx (g_debug_line_count * 2), 1); - g_debug_line_count = 0; + params.viewport = .[cast(float)w, cast(float)h, 0, 0]; + + flush_debug_lines(*gPipelines.debugline, g_debug_lines, *g_debug_line_count, *params); + flush_debug_lines(*gPipelines.debugline_overlay, g_overlay_lines, *g_overlay_line_count, *params); } + +#assert size_of(Debug_Line) == 44 "Debug_Line must stay tightly packed: the pipeline layout hardcodes its field offsets."; diff --git a/src/rendering/pipelines.jai b/src/rendering/pipelines.jai index 3b803ce..2c50c87 100644 --- a/src/rendering/pipelines.jai +++ b/src/rendering/pipelines.jai @@ -113,6 +113,8 @@ gPipelines : struct { sh_irradiance: Pipeline_Binding; debugline : Pipeline_Binding; + // Same shader, but depth testing disabled so editor gizmos draw on top. + debugline_overlay : Pipeline_Binding; } create_final_image :: () { @@ -1694,20 +1696,45 @@ create_particle_pipeline :: () { } create_debugline_pipeline :: () { - buf_desc := sg_buffer_desc.{ - size = DEBUG_LINE_MAX * 2 * 7 * size_of(float), - usage = .DYNAMIC, - label = "debug_line_verts", + // The two corner coordinates of a quad: x picks the side of the line, y picks + // the end. The vertex shader turns those into a screen-space widened line. + corners := float.[ + -1, 0, 1, 0, 1, 1, + -1, 0, 1, 1, -1, 1, + ]; + quad_desc := sg_buffer_desc.{ + data = .{ ptr = corners.data, size = size_of(type_of(corners)) }, + label = "debug_line_quad", }; - gPipelines.debugline.bind.vertex_buffers[0] = sg_make_buffer(*buf_desc); + quad_buffer := sg_make_buffer(*quad_desc); + + buf_desc := sg_buffer_desc.{ + size = DEBUG_LINE_MAX * size_of(Debug_Line), + usage = .DYNAMIC, + label = "debug_line_instances", + }; + gPipelines.debugline.bind.vertex_buffers[0] = quad_buffer; + gPipelines.debugline.bind.vertex_buffers[1] = sg_make_buffer(*buf_desc); + + overlay_buf_desc := sg_buffer_desc.{ + size = DEBUG_OVERLAY_LINE_MAX * size_of(Debug_Line), + usage = .DYNAMIC, + label = "debug_line_overlay_instances", + }; + gPipelines.debugline_overlay.bind.vertex_buffers[0] = quad_buffer; + gPipelines.debugline_overlay.bind.vertex_buffers[1] = sg_make_buffer(*overlay_buf_desc); pipeline : sg_pipeline_desc; shader_desc := debugline_shader_desc(sg_query_backend()); pipeline.shader = sg_make_shader(*shader_desc); - pipeline.primitive_type = .LINES; - pipeline.layout.buffers[0].stride = 28; - pipeline.layout.attrs[ATTR_debugline_a_pos] = .{ format = .FLOAT3, buffer_index = 0, offset = 0 }; - pipeline.layout.attrs[ATTR_debugline_a_col] = .{ format = .FLOAT4, buffer_index = 0, offset = 12 }; + pipeline.layout.buffers[0].stride = 2 * size_of(float); + pipeline.layout.buffers[1].stride = size_of(Debug_Line); + pipeline.layout.buffers[1].step_func = .PER_INSTANCE; + pipeline.layout.attrs[ATTR_debugline_a_corner] = .{ format = .FLOAT2, buffer_index = 0, offset = 0 }; + pipeline.layout.attrs[ATTR_debugline_a_pos_a] = .{ format = .FLOAT3, buffer_index = 1, offset = 0 }; + pipeline.layout.attrs[ATTR_debugline_a_pos_b] = .{ format = .FLOAT3, buffer_index = 1, offset = 12 }; + pipeline.layout.attrs[ATTR_debugline_a_col] = .{ format = .FLOAT4, buffer_index = 1, offset = 24 }; + pipeline.layout.attrs[ATTR_debugline_a_width] = .{ format = .FLOAT, buffer_index = 1, offset = 40 }; pipeline.depth = .{ write_enabled = false, compare = .LESS_EQUAL, @@ -1720,4 +1747,13 @@ create_debugline_pipeline :: () { pipeline.colors[0] = color_state; pipeline.label = "debugline_pipeline"; gPipelines.debugline.pipeline = sg_make_pipeline(*pipeline); + + pipeline.depth.compare = .ALWAYS; + pipeline.colors[0].blend = .{ + enabled = true, + src_factor_rgb = .SRC_ALPHA, + dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, + }; + pipeline.label = "debugline_overlay_pipeline"; + gPipelines.debugline_overlay.pipeline = sg_make_pipeline(*pipeline); } diff --git a/src/rendering/tasks.jai b/src/rendering/tasks.jai index c203ee5..74b5453 100644 --- a/src/rendering/tasks.jai +++ b/src/rendering/tasks.jai @@ -7,6 +7,7 @@ Rendering_Task_Type :: enum { SET_CAMERA; SET_LIGHT; TRILE; // We need to add an ability to invalidate buffer instead of updating it constantly. Also probably have a buffer for static world triles and one for moving ones. + TRILE_DYNAMIC; TRILE_RDM; TRIXELS; BILLBOARD; @@ -65,6 +66,19 @@ Rendering_Task_Trile :: struct { lod_index : s32 = 0; // 0 = full detail, 1 = 4^3 LOD, 2 = 2^3 LOD } +// Triles at arbitrary float world positions (entities, moving things). +// Shares the instance buffer and pipelines with the static TRILE path, so it +// participates in main/gbuffer/shadow/reflection just like world triles. +// chunk_key is the enclosing chunk, used only for SH probe lookup. +Rendering_Task_Trile_Dynamic :: struct { + #as using t : Rendering_Task; + t.type = .TRILE_DYNAMIC; + trile : string; + positions : []Vector4; // xyz = world position, w = orientation 0..23 + chunk_key : Chunk_Key; + worldConf : *World_Config; +} + Rendering_Task_Trile_RDM :: struct { #as using t : Rendering_Task; t.type = .TRILE_RDM; @@ -177,6 +191,22 @@ tasks_to_commands :: () { array_add(*render_command_buckets.gbuffer, drawPositionsCmd); array_add(*render_command_buckets.shadow, drawPositionsCmd); } + case .TRILE_DYNAMIC; + dynTask := (cast(*Rendering_Task_Trile_Dynamic)it); + addF32Cmd := New(Render_Command_Add_Trile_Positions_F32,, temp); + addF32Cmd.positions = dynTask.positions; + array_add(*render_command_buckets.setup, addF32Cmd); + drawDynCmd := New(Render_Command_Draw_Trile_Positions,, temp); + drawDynCmd.trile = dynTask.trile; + drawDynCmd.chunk_key = dynTask.chunk_key; + drawDynCmd.amount = cast(s32)dynTask.positions.count; + drawDynCmd.conf = dynTask.worldConf; + drawDynCmd.offset_index = trile_add_counter; + trile_add_counter += 1; + array_add(*render_command_buckets.reflection, drawDynCmd); + array_add(*render_command_buckets.main, drawDynCmd); + array_add(*render_command_buckets.gbuffer, drawDynCmd); + array_add(*render_command_buckets.shadow, drawDynCmd); case .TRILE_RDM; rdmTask := (cast(*Rendering_Task_Trile_RDM)it); addCmd := New(Render_Command_Add_Trile_RDM_Position,, temp); diff --git a/src/resource_mirror.jai b/src/resource_mirror.jai new file mode 100644 index 0000000..ad626ab --- /dev/null +++ b/src/resource_mirror.jai @@ -0,0 +1,162 @@ +// Packs are built from a throwaway copy of the resource trees, not from the +// trees themselves: +// +// wipe .build/generated -> copy the trees in -> process the copy -> pack it +// +// Processing (currently just the Aseprite export) writes its output into the +// copy, so the real resource trees only ever hold sources. Rebuilding the copy +// from scratch every time is what keeps this simple: there is no stale output to +// detect and nothing to prune, because a deleted source is simply not there. +// +// Used by both the build (meta/pack.jai) and the runtime hot-reloader +// (pack_hotreload.jai), so they process resources identically. + +#import "Basic"; +#import "String"; +#import "Process"; +#import "File"; +#import "File_Utilities"; + +// Not ".build/generated/resources": pack.jai derives the pack name by splitting +// on the *first* "/resources/", so a "resources" segment here would shadow the +// real one and yield pack "game" instead of "game_core". +MIRROR_ROOT :: ".build/generated"; + +ASEPRITE_SOURCE_EXTENSIONS :: string.["aseprite", "ase"]; + +is_aseprite_source :: (short_name: string) -> bool { + ok, base, extension := split_from_right(short_name, #char "."); + if !ok return false; + for ASEPRITE_SOURCE_EXTENSIONS if extension == it return true; + return false; +} + +// Wipes and rebuilds the mirror, runs the processing steps over it, and returns +// the mirrored resource roots to pack in place of the real ones. +prepare_resource_mirror :: (use_test_game: bool) -> [] string { + run_command("rm", "-rf", MIRROR_ROOT); + + game_tree := ifx use_test_game then "test_game" else "game"; + + roots: [..] string; + roots.allocator = temp; + array_add(*roots, copy_tree_into("./resources", MIRROR_ROOT)); + array_add(*roots, copy_tree_into(tprint("./%/resources", game_tree), tprint("%/%", MIRROR_ROOT, game_tree))); + + export_aseprite_sheets(roots); + + return roots; +} + +// "cp -r ./game/resources .build/generated/game" -> ".build/generated/game/resources" +copy_tree_into :: (source: string, destination_parent: string) -> string { + make_directory_if_it_does_not_exist(destination_parent, recursive = true); + run_command("cp", "-r", source, destination_parent); + + ok, parent, name := split_from_right(source, #char "/"); + return tprint("%/%", destination_parent, name); +} + +// Aseprite's CLI, reproducing byte for byte what its "Export Sprite Sheet" +// dialog was producing for the sheets that used to be checked in. +// --format json-array and --list-tags are load-bearing: asset_manager.jai parses +// `frames` as an array and drives Animation off `meta.frameTags`. +ASEPRITE_EXPORT_FLAGS :: string.[ + "--format", "json-array", + "--list-tags", + "--list-layers", + "--list-slices", +]; + +// "player.aseprite" -> player.sheet.png / player.sheet.json +// "anim.sheet.aseprite" -> anim.sheet.png / anim.sheet.json +// +// The doubled-up ".sheet" is dropped because asset_manager.jai keys sheets on +// everything before the *first* dot, so "anim.sheet.sheet.png" would load as a +// sheet named "anim" with extension "sheet.sheet.png" and be rejected. +sheet_output_paths :: (source: string) -> (png: string, json: string) { + ok, stem := split_from_right(source, #char "."); + if ends_with(stem, ".sheet") { + stem.count -= ".sheet".count; + } + return tprint("%.sheet.png", stem), tprint("%.sheet.json", stem); +} + +find_aseprite_binary :: () -> (path: string, found: bool) { + candidates: [..] string; + candidates.allocator = temp; + + #if OS == .LINUX || OS == .MACOS { + POSIX :: #import "POSIX"; + read_env :: (name: string) -> string { + value := POSIX.getenv(name.data); + if !value return ""; + return to_string(value); + } + + // ASEPRITE points the build at a different install without editing this. + override := read_env("ASEPRITE"); + if override array_add(*candidates, override); + + home := read_env("HOME"); + if home array_add(*candidates, tprint("%/bin/aseprite", home)); + } + + #if OS == .MACOS { + array_add(*candidates, "/Applications/Aseprite.app/Contents/MacOS/aseprite"); + } + + array_add(*candidates, "aseprite"); // whatever is on PATH + + for candidates { + // Probing by running it also catches a path that exists but isn't executable. + result := run_command(it, "--version", capture_and_return_output = true); + if result.type == .EXITED && result.exit_code == 0 return it, true; + } + return "", false; +} + +collect_aseprite_sources :: (root: string, sources: *[..] string) { + if !is_directory(root) return; + + visit_files(root, true, sources, (info: *File_Visit_Info, sources: *[..] string) { + if info.is_directory return; + if !is_aseprite_source(info.short_name) return; + array_add(sources, copy_string(info.full_name)); + }); +} + +// Exports every Aseprite file in the mirror to a sheet pair beside it. pack.jai +// skips the sources themselves, so only the exports end up in the pack. +export_aseprite_sheets :: (roots: [] string) { + sources: [..] string; + sources.allocator = temp; + for roots collect_aseprite_sources(it, *sources); + if !sources return; + + aseprite, found := find_aseprite_binary(); + if !found { + log_error("Aseprite export: % source file(s) to export but no aseprite binary was found.", sources.count); + log_error("Looked for $ASEPRITE, $HOME/bin/aseprite, and 'aseprite' on PATH."); + exit(1); + } + + for source: sources { + png, json := sheet_output_paths(source); + + args: [..] string; + args.allocator = temp; + array_add(*args, aseprite, "-b", source, "--sheet", png, "--data", json); + for flag: ASEPRITE_EXPORT_FLAGS array_add(*args, flag); + + result, output, error := run_command(..args, capture_and_return_output = true); + if result.type != .EXITED || result.exit_code != 0 { + log_error("Aseprite export failed for %", source); + if output log_error(output,, logger = runtime_support_default_logger); + if error log_error(error,, logger = runtime_support_default_logger); + exit(1); + } + } + + print("Aseprite: exported % sheet(s)\n", sources.count); +} diff --git a/src/shaders/jai/shader_debugline.jai b/src/shaders/jai/shader_debugline.jai index 20126a6..8eb1711 100644 --- a/src/shaders/jai/shader_debugline.jai +++ b/src/shaders/jai/shader_debugline.jai @@ -13,30 +13,55 @@ Vertex Shader: vs_debugline Fragment Shader: fs_debugline Attributes: - ATTR_debugline_a_pos => 0 - ATTR_debugline_a_col => 1 + ATTR_debugline_a_corner => 0 + ATTR_debugline_a_pos_a => 1 + ATTR_debugline_a_pos_b => 2 + ATTR_debugline_a_col => 3 + ATTR_debugline_a_width => 4 Bindings: Uniform block 'debugline_vs_params': Jai struct: Debugline_Vs_Params Bind slot: UB_debugline_vs_params => 0 */ -ATTR_debugline_a_pos :: 0; -ATTR_debugline_a_col :: 1; +ATTR_debugline_a_corner :: 0; +ATTR_debugline_a_pos_a :: 1; +ATTR_debugline_a_pos_b :: 2; +ATTR_debugline_a_col :: 3; +ATTR_debugline_a_width :: 4; UB_debugline_vs_params :: 0; Debugline_Vs_Params :: struct { mvp: [16]float; + viewport: [4]float; }; /* #version 430 - uniform vec4 debugline_vs_params[4]; - layout(location = 0) in vec3 a_pos; + uniform vec4 debugline_vs_params[5]; + layout(location = 1) in vec3 a_pos_a; + layout(location = 2) in vec3 a_pos_b; + layout(location = 0) in vec2 a_corner; + layout(location = 4) in float a_width; layout(location = 0) out vec4 v_col; - layout(location = 1) in vec4 a_col; + layout(location = 3) in vec4 a_col; void main() { - gl_Position = mat4(debugline_vs_params[0], debugline_vs_params[1], debugline_vs_params[2], debugline_vs_params[3]) * vec4(a_pos, 1.0); + mat4 _18 = mat4(debugline_vs_params[0], debugline_vs_params[1], debugline_vs_params[2], debugline_vs_params[3]); + vec4 _28 = _18 * vec4(a_pos_a, 1.0); + vec4 _38 = _18 * vec4(a_pos_b, 1.0); + vec2 _78 = ((_38.xy / vec2(max(_38.w, 9.9999997473787516355514526367188e-05))) * debugline_vs_params[4].xy) - ((_28.xy / vec2(max(_28.w, 9.9999997473787516355514526367188e-05))) * debugline_vs_params[4].xy); + float _81 = length(_78); + vec2 _86; + if (_81 > 9.9999997473787516355514526367188e-05) + { + _86 = _78 / vec2(_81); + } + else + { + _86 = vec2(1.0, 0.0); + } + vec4 _115 = mix(_28, _38, vec4(a_corner.y)); + gl_Position = _115 + vec4((((vec2(-_86.y, _86.x) * a_corner.x) * ((a_width * 0.5) + 0.5)) / debugline_vs_params[4].xy) * _115.w, 0.0, 0.0); v_col = a_col; } @@ -44,26 +69,74 @@ Debugline_Vs_Params :: struct { vs_debugline_source_glsl430 := u8.[ 0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x34,0x33,0x30,0x0a,0x0a,0x75,0x6e, 0x69,0x66,0x6f,0x72,0x6d,0x20,0x76,0x65,0x63,0x34,0x20,0x64,0x65,0x62,0x75,0x67, - 0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x34, + 0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x35, 0x5d,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69, - 0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x33,0x20, - 0x61,0x5f,0x70,0x6f,0x73,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f, - 0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x6f,0x75,0x74,0x20, - 0x76,0x65,0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x6c,0x61,0x79,0x6f, - 0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29, - 0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a, - 0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20, + 0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x33,0x20, + 0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28, + 0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x32,0x29,0x20,0x69,0x6e, + 0x20,0x76,0x65,0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62,0x3b,0x0a,0x6c, + 0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d, + 0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x61,0x5f,0x63,0x6f, + 0x72,0x6e,0x65,0x72,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63, + 0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x34,0x29,0x20,0x69,0x6e,0x20,0x66,0x6c, + 0x6f,0x61,0x74,0x20,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x3b,0x0a,0x6c,0x61,0x79, + 0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30, + 0x29,0x20,0x6f,0x75,0x74,0x20,0x76,0x65,0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c, + 0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f, + 0x6e,0x20,0x3d,0x20,0x33,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61, + 0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e, + 0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x74,0x34,0x20,0x5f,0x31, + 0x38,0x20,0x3d,0x20,0x6d,0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69, + 0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c, + 0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61, + 0x72,0x61,0x6d,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69, + 0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c, + 0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61, + 0x72,0x61,0x6d,0x73,0x5b,0x33,0x5d,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65, + 0x63,0x34,0x20,0x5f,0x32,0x38,0x20,0x3d,0x20,0x5f,0x31,0x38,0x20,0x2a,0x20,0x76, + 0x65,0x63,0x34,0x28,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x2c,0x20,0x31,0x2e,0x30, + 0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x34,0x20,0x5f,0x33,0x38,0x20, + 0x3d,0x20,0x5f,0x31,0x38,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70, + 0x6f,0x73,0x5f,0x62,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20, + 0x76,0x65,0x63,0x32,0x20,0x5f,0x37,0x38,0x20,0x3d,0x20,0x28,0x28,0x5f,0x33,0x38, + 0x2e,0x78,0x79,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x6d,0x61,0x78,0x28,0x5f, + 0x33,0x38,0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37,0x34, + 0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35,0x32, + 0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20,0x2a, + 0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61, + 0x72,0x61,0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x20,0x2d,0x20,0x28,0x28, + 0x5f,0x32,0x38,0x2e,0x78,0x79,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x6d,0x61, + 0x78,0x28,0x5f,0x32,0x38,0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39, + 0x39,0x37,0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31, + 0x34,0x35,0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29, + 0x29,0x20,0x2a,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73, + 0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x3b,0x0a, + 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x38,0x31,0x20,0x3d,0x20, + 0x6c,0x65,0x6e,0x67,0x74,0x68,0x28,0x5f,0x37,0x38,0x29,0x3b,0x0a,0x20,0x20,0x20, + 0x20,0x76,0x65,0x63,0x32,0x20,0x5f,0x38,0x36,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69, + 0x66,0x20,0x28,0x5f,0x38,0x31,0x20,0x3e,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39, + 0x39,0x37,0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31, + 0x34,0x35,0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x0a, + 0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x38, + 0x36,0x20,0x3d,0x20,0x5f,0x37,0x38,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x5f, + 0x38,0x31,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65, + 0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20, + 0x20,0x20,0x5f,0x38,0x36,0x20,0x3d,0x20,0x76,0x65,0x63,0x32,0x28,0x31,0x2e,0x30, + 0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20, + 0x20,0x20,0x76,0x65,0x63,0x34,0x20,0x5f,0x31,0x31,0x35,0x20,0x3d,0x20,0x6d,0x69, + 0x78,0x28,0x5f,0x32,0x38,0x2c,0x20,0x5f,0x33,0x38,0x2c,0x20,0x76,0x65,0x63,0x34, + 0x28,0x61,0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x79,0x29,0x29,0x3b,0x0a,0x20, 0x20,0x20,0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d, - 0x20,0x6d,0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f, - 0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x64,0x65, - 0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d, - 0x73,0x5b,0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f, - 0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x64,0x65, - 0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d, - 0x73,0x5b,0x33,0x5d,0x29,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70, - 0x6f,0x73,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x5f, - 0x63,0x6f,0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x7d,0x0a,0x0a, - 0x00, + 0x20,0x5f,0x31,0x31,0x35,0x20,0x2b,0x20,0x76,0x65,0x63,0x34,0x28,0x28,0x28,0x28, + 0x76,0x65,0x63,0x32,0x28,0x2d,0x5f,0x38,0x36,0x2e,0x79,0x2c,0x20,0x5f,0x38,0x36, + 0x2e,0x78,0x29,0x20,0x2a,0x20,0x61,0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x78, + 0x29,0x20,0x2a,0x20,0x28,0x28,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x20,0x2a,0x20, + 0x30,0x2e,0x35,0x29,0x20,0x2b,0x20,0x30,0x2e,0x35,0x29,0x29,0x20,0x2f,0x20,0x64, + 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, + 0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x20,0x2a,0x20,0x5f,0x31,0x31,0x35, + 0x2e,0x77,0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20, + 0x20,0x20,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c, + 0x3b,0x0a,0x7d,0x0a,0x0a,0x00, ]; /* #version 430 @@ -91,14 +164,32 @@ fs_debugline_source_glsl430 := u8.[ /* #version 300 es - uniform vec4 debugline_vs_params[4]; - layout(location = 0) in vec3 a_pos; + uniform vec4 debugline_vs_params[5]; + layout(location = 1) in vec3 a_pos_a; + layout(location = 2) in vec3 a_pos_b; + layout(location = 0) in vec2 a_corner; + layout(location = 4) in float a_width; out vec4 v_col; - layout(location = 1) in vec4 a_col; + layout(location = 3) in vec4 a_col; void main() { - gl_Position = mat4(debugline_vs_params[0], debugline_vs_params[1], debugline_vs_params[2], debugline_vs_params[3]) * vec4(a_pos, 1.0); + mat4 _18 = mat4(debugline_vs_params[0], debugline_vs_params[1], debugline_vs_params[2], debugline_vs_params[3]); + vec4 _28 = _18 * vec4(a_pos_a, 1.0); + vec4 _38 = _18 * vec4(a_pos_b, 1.0); + vec2 _78 = ((_38.xy / vec2(max(_38.w, 9.9999997473787516355514526367188e-05))) * debugline_vs_params[4].xy) - ((_28.xy / vec2(max(_28.w, 9.9999997473787516355514526367188e-05))) * debugline_vs_params[4].xy); + float _81 = length(_78); + vec2 _86; + if (_81 > 9.9999997473787516355514526367188e-05) + { + _86 = _78 / vec2(_81); + } + else + { + _86 = vec2(1.0, 0.0); + } + vec4 _115 = mix(_28, _38, vec4(a_corner.y)); + gl_Position = _115 + vec4((((vec2(-_86.y, _86.x) * a_corner.x) * ((a_width * 0.5) + 0.5)) / debugline_vs_params[4].xy) * _115.w, 0.0, 0.0); v_col = a_col; } @@ -107,23 +198,72 @@ vs_debugline_source_glsl300es := u8.[ 0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x30,0x30,0x20,0x65,0x73,0x0a, 0x0a,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x20,0x76,0x65,0x63,0x34,0x20,0x64,0x65, 0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d, - 0x73,0x5b,0x34,0x5d,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63, - 0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65, - 0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x3b,0x0a,0x6f,0x75,0x74,0x20,0x76,0x65, - 0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74, - 0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29,0x20,0x69, - 0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x0a,0x76, - 0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20, - 0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x6d, - 0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73, - 0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75, - 0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b, - 0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73, - 0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75, - 0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b, - 0x33,0x5d,0x29,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70,0x6f,0x73, - 0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x5f,0x63,0x6f, - 0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x7d,0x0a,0x0a,0x00, + 0x73,0x5b,0x35,0x5d,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63, + 0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29,0x20,0x69,0x6e,0x20,0x76,0x65, + 0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x3b,0x0a,0x6c,0x61,0x79,0x6f, + 0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x32,0x29, + 0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62, + 0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f, + 0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x61, + 0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28, + 0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x34,0x29,0x20,0x69,0x6e, + 0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x3b,0x0a, + 0x6f,0x75,0x74,0x20,0x76,0x65,0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x3b,0x0a, + 0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20, + 0x3d,0x20,0x33,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61,0x5f,0x63, + 0x6f,0x6c,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29, + 0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x74,0x34,0x20,0x5f,0x31,0x38,0x20, + 0x3d,0x20,0x6d,0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65, + 0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x64, + 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, + 0x6d,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65, + 0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x64, + 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, + 0x6d,0x73,0x5b,0x33,0x5d,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x34, + 0x20,0x5f,0x32,0x38,0x20,0x3d,0x20,0x5f,0x31,0x38,0x20,0x2a,0x20,0x76,0x65,0x63, + 0x34,0x28,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b, + 0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x34,0x20,0x5f,0x33,0x38,0x20,0x3d,0x20, + 0x5f,0x31,0x38,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70,0x6f,0x73, + 0x5f,0x62,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65, + 0x63,0x32,0x20,0x5f,0x37,0x38,0x20,0x3d,0x20,0x28,0x28,0x5f,0x33,0x38,0x2e,0x78, + 0x79,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x6d,0x61,0x78,0x28,0x5f,0x33,0x38, + 0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37,0x34,0x37,0x33, + 0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35,0x32,0x36,0x33, + 0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20,0x2a,0x20,0x64, + 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, + 0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x20,0x2d,0x20,0x28,0x28,0x5f,0x32, + 0x38,0x2e,0x78,0x79,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x6d,0x61,0x78,0x28, + 0x5f,0x32,0x38,0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37, + 0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35, + 0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20, + 0x2a,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70, + 0x61,0x72,0x61,0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x3b,0x0a,0x20,0x20, + 0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x38,0x31,0x20,0x3d,0x20,0x6c,0x65, + 0x6e,0x67,0x74,0x68,0x28,0x5f,0x37,0x38,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76, + 0x65,0x63,0x32,0x20,0x5f,0x38,0x36,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x66,0x20, + 0x28,0x5f,0x38,0x31,0x20,0x3e,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37, + 0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35, + 0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x0a,0x20,0x20, + 0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x38,0x36,0x20, + 0x3d,0x20,0x5f,0x37,0x38,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x5f,0x38,0x31, + 0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,0x73, + 0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20, + 0x5f,0x38,0x36,0x20,0x3d,0x20,0x76,0x65,0x63,0x32,0x28,0x31,0x2e,0x30,0x2c,0x20, + 0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20, + 0x76,0x65,0x63,0x34,0x20,0x5f,0x31,0x31,0x35,0x20,0x3d,0x20,0x6d,0x69,0x78,0x28, + 0x5f,0x32,0x38,0x2c,0x20,0x5f,0x33,0x38,0x2c,0x20,0x76,0x65,0x63,0x34,0x28,0x61, + 0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x79,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20, + 0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f, + 0x31,0x31,0x35,0x20,0x2b,0x20,0x76,0x65,0x63,0x34,0x28,0x28,0x28,0x28,0x76,0x65, + 0x63,0x32,0x28,0x2d,0x5f,0x38,0x36,0x2e,0x79,0x2c,0x20,0x5f,0x38,0x36,0x2e,0x78, + 0x29,0x20,0x2a,0x20,0x61,0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x78,0x29,0x20, + 0x2a,0x20,0x28,0x28,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x20,0x2a,0x20,0x30,0x2e, + 0x35,0x29,0x20,0x2b,0x20,0x30,0x2e,0x35,0x29,0x29,0x20,0x2f,0x20,0x64,0x65,0x62, + 0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73, + 0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x20,0x2a,0x20,0x5f,0x31,0x31,0x35,0x2e,0x77, + 0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20, + 0x20,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a, + 0x7d,0x0a,0x0a,0x00, ]; /* #version 300 es @@ -161,6 +301,7 @@ fs_debugline_source_glsl300es := u8.[ struct debugline_vs_params { float4x4 mvp; + float4 viewport; }; struct main0_out @@ -171,14 +312,31 @@ fs_debugline_source_glsl300es := u8.[ struct main0_in { - float3 a_pos [[attribute(0)]]; - float4 a_col [[attribute(1)]]; + float2 a_corner [[attribute(0)]]; + float3 a_pos_a [[attribute(1)]]; + float3 a_pos_b [[attribute(2)]]; + float4 a_col [[attribute(3)]]; + float a_width [[attribute(4)]]; }; - vertex main0_out main0(main0_in in [[stage_in]], constant debugline_vs_params& _19 [[buffer(0)]]) + vertex main0_out main0(main0_in in [[stage_in]], constant debugline_vs_params& _13 [[buffer(0)]]) { main0_out out = {}; - out.gl_Position = _19.mvp * float4(in.a_pos, 1.0); + float4 _28 = _13.mvp * float4(in.a_pos_a, 1.0); + float4 _38 = _13.mvp * float4(in.a_pos_b, 1.0); + float2 _78 = ((_38.xy / float2(fast::max(_38.w, 9.9999997473787516355514526367188e-05))) * _13.viewport.xy) - ((_28.xy / float2(fast::max(_28.w, 9.9999997473787516355514526367188e-05))) * _13.viewport.xy); + float _81 = length(_78); + float2 _86; + if (_81 > 9.9999997473787516355514526367188e-05) + { + _86 = _78 / float2(_81); + } + else + { + _86 = float2(1.0, 0.0); + } + float4 _115 = mix(_28, _38, float4(in.a_corner.y)); + out.gl_Position = _115 + float4((((float2(-_86.y, _86.x) * in.a_corner.x) * ((in.a_width * 0.5) + 0.5)) / _13.viewport.xy) * _115.w, 0.0, 0.0); out.v_col = in.a_col; return out; } @@ -192,33 +350,82 @@ vs_debugline_source_metal_macos := u8.[ 0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x64, 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, 0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x78, - 0x34,0x20,0x6d,0x76,0x70,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63, - 0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20, - 0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x5b, - 0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,0x5d,0x5d,0x3b,0x0a, - 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x67,0x6c,0x5f,0x50,0x6f, - 0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f, - 0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20, - 0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66, - 0x6c,0x6f,0x61,0x74,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x20,0x5b,0x5b,0x61,0x74, - 0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20, - 0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x20,0x5b, - 0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x31,0x29,0x5d,0x5d,0x3b, - 0x0a,0x7d,0x3b,0x0a,0x0a,0x76,0x65,0x72,0x74,0x65,0x78,0x20,0x6d,0x61,0x69,0x6e, - 0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e, - 0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f, - 0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,0x64, - 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, - 0x6d,0x73,0x26,0x20,0x5f,0x31,0x39,0x20,0x5b,0x5b,0x62,0x75,0x66,0x66,0x65,0x72, - 0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x69, - 0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,0x20,0x7b,0x7d,0x3b, - 0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69, - 0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,0x31,0x39,0x2e,0x6d,0x76,0x70,0x20,0x2a, - 0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,0x69,0x6e,0x2e,0x61,0x5f,0x70,0x6f,0x73, - 0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e, - 0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d,0x20,0x69,0x6e,0x2e,0x61,0x5f,0x63,0x6f,0x6c, - 0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74, - 0x3b,0x0a,0x7d,0x0a,0x0a,0x00, + 0x34,0x20,0x6d,0x76,0x70,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74, + 0x34,0x20,0x76,0x69,0x65,0x77,0x70,0x6f,0x72,0x74,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a, + 0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74, + 0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x76,0x5f, + 0x63,0x6f,0x6c,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30, + 0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20, + 0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f, + 0x73,0x69,0x74,0x69,0x6f,0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74, + 0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a, + 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x61,0x5f,0x63,0x6f,0x72, + 0x6e,0x65,0x72,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28, + 0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x33, + 0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69, + 0x62,0x75,0x74,0x65,0x28,0x31,0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66, + 0x6c,0x6f,0x61,0x74,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62,0x20,0x5b,0x5b, + 0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x32,0x29,0x5d,0x5d,0x3b,0x0a, + 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c, + 0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x33,0x29,0x5d, + 0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x61,0x5f,0x77, + 0x69,0x64,0x74,0x68,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65, + 0x28,0x34,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x76,0x65,0x72,0x74,0x65, + 0x78,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e, + 0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b, + 0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73, + 0x74,0x61,0x6e,0x74,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76, + 0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x26,0x20,0x5f,0x31,0x33,0x20,0x5b,0x5b, + 0x62,0x75,0x66,0x66,0x65,0x72,0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20, + 0x20,0x20,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74, + 0x20,0x3d,0x20,0x7b,0x7d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74, + 0x34,0x20,0x5f,0x32,0x38,0x20,0x3d,0x20,0x5f,0x31,0x33,0x2e,0x6d,0x76,0x70,0x20, + 0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,0x69,0x6e,0x2e,0x61,0x5f,0x70,0x6f, + 0x73,0x5f,0x61,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66, + 0x6c,0x6f,0x61,0x74,0x34,0x20,0x5f,0x33,0x38,0x20,0x3d,0x20,0x5f,0x31,0x33,0x2e, + 0x6d,0x76,0x70,0x20,0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,0x69,0x6e,0x2e, + 0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20, + 0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x5f,0x37,0x38,0x20,0x3d,0x20, + 0x28,0x28,0x5f,0x33,0x38,0x2e,0x78,0x79,0x20,0x2f,0x20,0x66,0x6c,0x6f,0x61,0x74, + 0x32,0x28,0x66,0x61,0x73,0x74,0x3a,0x3a,0x6d,0x61,0x78,0x28,0x5f,0x33,0x38,0x2e, + 0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37,0x34,0x37,0x33,0x37, + 0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35,0x32,0x36,0x33,0x36, + 0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20,0x2a,0x20,0x5f,0x31, + 0x33,0x2e,0x76,0x69,0x65,0x77,0x70,0x6f,0x72,0x74,0x2e,0x78,0x79,0x29,0x20,0x2d, + 0x20,0x28,0x28,0x5f,0x32,0x38,0x2e,0x78,0x79,0x20,0x2f,0x20,0x66,0x6c,0x6f,0x61, + 0x74,0x32,0x28,0x66,0x61,0x73,0x74,0x3a,0x3a,0x6d,0x61,0x78,0x28,0x5f,0x32,0x38, + 0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37,0x34,0x37,0x33, + 0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35,0x32,0x36,0x33, + 0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20,0x2a,0x20,0x5f, + 0x31,0x33,0x2e,0x76,0x69,0x65,0x77,0x70,0x6f,0x72,0x74,0x2e,0x78,0x79,0x29,0x3b, + 0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x38,0x31,0x20,0x3d, + 0x20,0x6c,0x65,0x6e,0x67,0x74,0x68,0x28,0x5f,0x37,0x38,0x29,0x3b,0x0a,0x20,0x20, + 0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x5f,0x38,0x36,0x3b,0x0a,0x20,0x20, + 0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x38,0x31,0x20,0x3e,0x20,0x39,0x2e,0x39,0x39, + 0x39,0x39,0x39,0x39,0x37,0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35, + 0x35,0x35,0x31,0x34,0x35,0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30, + 0x35,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20, + 0x20,0x5f,0x38,0x36,0x20,0x3d,0x20,0x5f,0x37,0x38,0x20,0x2f,0x20,0x66,0x6c,0x6f, + 0x61,0x74,0x32,0x28,0x5f,0x38,0x31,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a, + 0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20, + 0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x38,0x36,0x20,0x3d,0x20,0x66,0x6c,0x6f, + 0x61,0x74,0x32,0x28,0x31,0x2e,0x30,0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20, + 0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20, + 0x5f,0x31,0x31,0x35,0x20,0x3d,0x20,0x6d,0x69,0x78,0x28,0x5f,0x32,0x38,0x2c,0x20, + 0x5f,0x33,0x38,0x2c,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,0x69,0x6e,0x2e,0x61, + 0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x79,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20, + 0x20,0x6f,0x75,0x74,0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e, + 0x20,0x3d,0x20,0x5f,0x31,0x31,0x35,0x20,0x2b,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34, + 0x28,0x28,0x28,0x28,0x66,0x6c,0x6f,0x61,0x74,0x32,0x28,0x2d,0x5f,0x38,0x36,0x2e, + 0x79,0x2c,0x20,0x5f,0x38,0x36,0x2e,0x78,0x29,0x20,0x2a,0x20,0x69,0x6e,0x2e,0x61, + 0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x78,0x29,0x20,0x2a,0x20,0x28,0x28,0x69, + 0x6e,0x2e,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x20,0x2a,0x20,0x30,0x2e,0x35,0x29, + 0x20,0x2b,0x20,0x30,0x2e,0x35,0x29,0x29,0x20,0x2f,0x20,0x5f,0x31,0x33,0x2e,0x76, + 0x69,0x65,0x77,0x70,0x6f,0x72,0x74,0x2e,0x78,0x79,0x29,0x20,0x2a,0x20,0x5f,0x31, + 0x31,0x35,0x2e,0x77,0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b, + 0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d, + 0x20,0x69,0x6e,0x2e,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72, + 0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00, ]; /* #include @@ -276,14 +483,20 @@ debugline_shader_desc :: (backend: sg_backend) -> sg_shader_desc { desc.fragment_func.source = xx *fs_debugline_source_glsl430; desc.fragment_func.entry = "main"; desc.attrs[0].base_type = .FLOAT; - desc.attrs[0].glsl_name = "a_pos"; + desc.attrs[0].glsl_name = "a_corner"; desc.attrs[1].base_type = .FLOAT; - desc.attrs[1].glsl_name = "a_col"; + desc.attrs[1].glsl_name = "a_pos_a"; + desc.attrs[2].base_type = .FLOAT; + desc.attrs[2].glsl_name = "a_pos_b"; + desc.attrs[3].base_type = .FLOAT; + desc.attrs[3].glsl_name = "a_col"; + desc.attrs[4].base_type = .FLOAT; + desc.attrs[4].glsl_name = "a_width"; desc.uniform_blocks[0].stage = .VERTEX; desc.uniform_blocks[0].layout = .STD140; - desc.uniform_blocks[0].size = 64; + desc.uniform_blocks[0].size = 80; desc.uniform_blocks[0].glsl_uniforms[0].type = .FLOAT4; - desc.uniform_blocks[0].glsl_uniforms[0].array_count = 4; + desc.uniform_blocks[0].glsl_uniforms[0].array_count = 5; desc.uniform_blocks[0].glsl_uniforms[0].glsl_name = "debugline_vs_params"; case .GLES3; desc.vertex_func.source = xx *vs_debugline_source_glsl300es; @@ -291,14 +504,20 @@ debugline_shader_desc :: (backend: sg_backend) -> sg_shader_desc { desc.fragment_func.source = xx *fs_debugline_source_glsl300es; desc.fragment_func.entry = "main"; desc.attrs[0].base_type = .FLOAT; - desc.attrs[0].glsl_name = "a_pos"; + desc.attrs[0].glsl_name = "a_corner"; desc.attrs[1].base_type = .FLOAT; - desc.attrs[1].glsl_name = "a_col"; + desc.attrs[1].glsl_name = "a_pos_a"; + desc.attrs[2].base_type = .FLOAT; + desc.attrs[2].glsl_name = "a_pos_b"; + desc.attrs[3].base_type = .FLOAT; + desc.attrs[3].glsl_name = "a_col"; + desc.attrs[4].base_type = .FLOAT; + desc.attrs[4].glsl_name = "a_width"; desc.uniform_blocks[0].stage = .VERTEX; desc.uniform_blocks[0].layout = .STD140; - desc.uniform_blocks[0].size = 64; + desc.uniform_blocks[0].size = 80; desc.uniform_blocks[0].glsl_uniforms[0].type = .FLOAT4; - desc.uniform_blocks[0].glsl_uniforms[0].array_count = 4; + desc.uniform_blocks[0].glsl_uniforms[0].array_count = 5; desc.uniform_blocks[0].glsl_uniforms[0].glsl_name = "debugline_vs_params"; case .METAL_MACOS; desc.vertex_func.source = xx *vs_debugline_source_metal_macos; @@ -307,9 +526,12 @@ debugline_shader_desc :: (backend: sg_backend) -> sg_shader_desc { desc.fragment_func.entry = "main0"; desc.attrs[0].base_type = .FLOAT; desc.attrs[1].base_type = .FLOAT; + desc.attrs[2].base_type = .FLOAT; + desc.attrs[3].base_type = .FLOAT; + desc.attrs[4].base_type = .FLOAT; desc.uniform_blocks[0].stage = .VERTEX; desc.uniform_blocks[0].layout = .STD140; - desc.uniform_blocks[0].size = 64; + desc.uniform_blocks[0].size = 80; desc.uniform_blocks[0].msl_buffer_n = 0; } return desc; diff --git a/src/shaders/shader_debugline.glsl b/src/shaders/shader_debugline.glsl index d6c04aa..a2dd04b 100644 --- a/src/shaders/shader_debugline.glsl +++ b/src/shaders/shader_debugline.glsl @@ -1,16 +1,44 @@ @vs vs_debugline -in vec3 a_pos; +// Lines are drawn as camera-facing quads rather than GL lines, so they can have +// a real pixel width. Buffer 0 holds the six corners of a unit quad, buffer 1 +// one instance per line. +in vec2 a_corner; // x = which side (-1 / +1), y = which end (0 = a, 1 = b) +in vec3 a_pos_a; +in vec3 a_pos_b; in vec4 a_col; +in float a_width; // in pixels layout(binding=0) uniform debugline_vs_params { mat4 mvp; + vec4 viewport; // xy = size in pixels, zw = unused }; out vec4 v_col; void main() { - gl_Position = mvp * vec4(a_pos, 1.0); + vec4 clip_a = mvp * vec4(a_pos_a, 1.0); + vec4 clip_b = mvp * vec4(a_pos_b, 1.0); + + // Widen in screen space so the line keeps its pixel width at any depth. + // Guard against w <= 0: an endpoint behind the eye has no screen position, + // so fall back to the other end's and let clipping handle the rest. + float wa = max(clip_a.w, 0.0001); + float wb = max(clip_b.w, 0.0001); + vec2 screen_a = (clip_a.xy / wa) * viewport.xy; + vec2 screen_b = (clip_b.xy / wb) * viewport.xy; + + vec2 delta = screen_b - screen_a; + float len = length(delta); + vec2 dir = (len > 0.0001) ? delta / len : vec2(1.0, 0.0); + vec2 normal = vec2(-dir.y, dir.x); + + vec4 clip = mix(clip_a, clip_b, a_corner.y); + // Half a pixel of extra width keeps thin lines from disappearing between + // sample points. + vec2 offset = normal * a_corner.x * (a_width * 0.5 + 0.5) / viewport.xy; + gl_Position = clip + vec4(offset * clip.w, 0.0, 0.0); + v_col = a_col; } diff --git a/src/tests/exe_tests/runner.jai b/src/tests/exe_tests/runner.jai index 3019e95..db4cca1 100644 --- a/src/tests/exe_tests/runner.jai +++ b/src/tests/exe_tests/runner.jai @@ -1,15 +1,39 @@ +// A single test gets this long to run its commands before it is declared hung +// and the run moves on. This only fires while frames are still being produced; +// a genuine freeze has to be caught by a timeout around the process itself, +// which is what run_tests.sh does. +EXE_TEST_TIMEOUT_SECONDS :: 30.0; + Exe_Runner :: struct { - suite_idx : int; - test_idx : int; - cmd_idx : int; - wait_until : float64; - test_failed : bool; - test_started : bool; - done : bool; + suite_idx : int; + test_idx : int; + cmd_idx : int; + wait_until : float64; + test_failed : bool; + test_started : bool; + done : bool; + passed : int; + failed : int; + test_deadline : float64; } g_exe_runner : Exe_Runner; +// Ends the process with a status that reflects the run, so a failing exe test +// actually fails the build instead of quitting quietly with 0. +finish_exe_tests :: (r: *Exe_Runner) { + total := r.passed + r.failed; + if r.failed == 0 { + print("[exe tests] All % test(s) passed.\n", total); + } else { + print("[exe tests] %/% passed, % FAILED.\n", r.passed, total, r.failed); + } + r.done = true; + sapp_request_quit(); + status : s32 = ifx r.failed > 0 then cast(s32) 1 else cast(s32) 0; + exit(status); +} + run_exe_tests :: () { r := *g_exe_runner; if r.done then return; @@ -20,8 +44,7 @@ run_exe_tests :: () { if r.suite_idx >= g_test_runner_state.count { print("[exe tests] All suites complete.\n"); - r.done = true; - sapp_request_quit(); + finish_exe_tests(r); return; } @@ -40,15 +63,30 @@ run_exe_tests :: () { if !r.test_started { print("[exe tests] Starting '%' / '%'\n", suite.name, test.name); - r.test_started = true; + r.test_started = true; + r.test_deadline = get_time() + EXE_TEST_TIMEOUT_SECONDS; + } + + // A test that outlives its budget is failed where it stands rather than + // being allowed to stall the whole run. + if get_time() > r.test_deadline { + print("[exe tests] TIMEOUT '%' / '%' after %s (cmd % of %)\n", + suite.name, test.name, EXE_TEST_TIMEOUT_SECONDS, r.cmd_idx, test.cmds.count); + r.test_failed = true; + r.cmd_idx = test.cmds.count; } if r.cmd_idx >= test.cmds.count { if r.test_failed { + r.failed += 1; print("[exe tests] FAIL '%' / '%'\n", suite.name, test.name); } else { + r.passed += 1; print("[exe tests] PASS '%' / '%'\n", suite.name, test.name); } + // Cleared here too: timing out mid-WAIT leaves a stale deadline behind, + // which would make the next test's first WAIT elapse instantly. + r.wait_until = 0; r.test_idx += 1; r.cmd_idx = 0; r.test_failed = false; diff --git a/src/tests/framework.jai b/src/tests/framework.jai new file mode 100644 index 0000000..8224df4 --- /dev/null +++ b/src/tests/framework.jai @@ -0,0 +1,12 @@ +// Test framework only — no test cases. This is loaded under every test flag so +// that the game side can write suites and exe tests exactly like the engine does. +// +// Unit tests need nothing beyond this: put them in a #if FLAG_TEST_GAME block +// next to the code they cover, with a local #run to call them, and a failing +// check turns into a compile error. +// +// Exe tests are registered from game_exe_tests_add(), which the engine calls +// during init when built with test_exe_game. + +#load "utils.jai"; +#load "exe_tests/index.jai"; diff --git a/src/tests/index.jai b/src/tests/index.jai index e2c863b..279eaaa 100644 --- a/src/tests/index.jai +++ b/src/tests/index.jai @@ -1,6 +1,7 @@ -#load "utils.jai"; +// Engine test cases. The framework itself lives in framework.jai, which is +// loaded separately so the game side gets it too. + #load "world_test.jai"; #load "../editor/rdm_disk_test.jai"; #load "engine_exe_tests/index.jai"; -#load "exe_tests/index.jai"; diff --git a/src/tests/world_test.jai b/src/tests/world_test.jai index 5ea94fd..a2bf76c 100644 --- a/src/tests/world_test.jai +++ b/src/tests/world_test.jai @@ -268,11 +268,106 @@ test_legacy_load_cursor_fix :: () { end_suite(s); } -#run { +// Entity types come from the game, so these run against whatever the game +// declares first rather than naming a type the engine cannot know about. +test_entity_save_load_roundtrip :: () { + s := begin_suite("entity save/load roundtrip"); + if ENTITY_TYPE_TABLE.count == 0 { + end_suite(s); + return; + } + type_name := ENTITY_TYPE_TABLE[0].name; + + world := make_test_world(); + a := spawn_entity(*world, type_name, .{3.5, 1.0, -2.0}, 17); + b := spawn_entity(*world, type_name, .{0.0, 0.0, 0.0}); + check(*s, "spawning returns entities", a != null && b != null); + if a == null || b == null { + end_suite(s); + return; + } + check(*s, "spawned entities get distinct ids", a.id != b.id); + + saved_fields := entity_fields_to_strings(a); + + json_str, bin_data := save_world(*world); + bin_bytes: []u8; + bin_bytes.data = bin_data.data; + bin_bytes.count = bin_data.count; + + loaded, ok := load_world_from_json(json_str, bin_bytes); + check(*s, "load succeeds", ok); + check(*s, "both entities come back", loaded.entities.count == 2); + if loaded.entities.count == 2 { + la := loaded.entities[0]; + check(*s, "type survives", la.type == a.type); + check(*s, "id survives", la.id == a.id); + check(*s, "position survives", la.position == Vector3.{3.5, 1.0, -2.0}); + check(*s, "orientation survives", la.orientation == 17); + + loaded_fields := entity_fields_to_strings(la); + same := loaded_fields.count == saved_fields.count; + for i: 0..min(loaded_fields.count, saved_fields.count)-1 { + if loaded_fields[i].name != saved_fields[i].name then same = false; + if loaded_fields[i].value != saved_fields[i].value then same = false; + } + check(*s, "editable fields survive", same); + } + + // Handing out an id a loaded entity already owns would make two entities + // indistinguishable to anything that refers to them by id. + check(*s, "the next id clears every loaded one", loaded.next_entity_id > max(a.id, b.id)); + + // The rest of the world is untouched by the entities riding along with it. + check(*s, "notes still load", loaded.notes.count == 1); + check(*s, "emitters still load", loaded.emitter_instances.count == 1); + + end_suite(s); +} + +// Saves are name-keyed so that a build with fewer types or fields than the one +// that wrote them still loads. Both halves of that log and carry on. +test_entity_unknown_names :: () { + s := begin_suite("entities tolerate names from other builds"); + if ENTITY_TYPE_TABLE.count == 0 { + end_suite(s); + return; + } + + world := make_test_world(); + e := spawn_entity(*world, ENTITY_TYPE_TABLE[0].name, .{1.0, 2.0, 3.0}); + check(*s, "the known type spawns", e != null); + if e == null { + end_suite(s); + return; + } + + // Expect one logged error here: that is the reported half of "skipped". + gone := spawn_entity(*world, "A_Type_That_Went_Away", .{0.0, 0.0, 0.0}); + check(*s, "an unknown type spawns nothing", gone == null); + check(*s, "and adds nothing to the world", world.entities.count == 1); + + before := entity_fields_to_strings(e); + entity_apply_field(e, "a_field_that_went_away", "12"); + after := entity_fields_to_strings(e); + unchanged := before.count == after.count; + for i: 0..min(before.count, after.count)-1 { + if before[i].value != after[i].value then unchanged = false; + } + check(*s, "an unknown field name changes nothing", unchanged); + + end_suite(s); +} + +// Stallable: the entity tests read ENTITY_TYPE_TABLE, which cannot be built +// until the metaprogram has collected every @Entity in the program. +#run,stallable { test_floor_div_mod(); test_coord_roundtrip(); test_chunk_coord_values(); test_world_save_load_roundtrip(); test_world_json_chunk_offsets(); test_legacy_load_cursor_fix(); + test_entity_save_load_roundtrip(); + test_entity_unknown_names(); } diff --git a/src/ui/autoedit.jai b/src/ui/autoedit.jai index f35d5b8..324b68a 100644 --- a/src/ui/autoedit.jai +++ b/src/ui/autoedit.jai @@ -59,18 +59,59 @@ note_to_autoedit_conf :: (notes: []string) -> Autoedit_Conf { return .{}; } -input_code_from_type_and_notes :: (name: string, type: *Type_Info, notes: []string) -> string { +// Types we can generate an editor widget for. Anything else (nested structs, +// arrays, the '#as using base' of an entity, ...) is skipped entirely. +autoedit_supports_type :: (type: *Type_Info) -> bool { + return type == type_info(float) || type == type_info(s32) || type == type_info(bool) + || type == type_info(Vector3) || type == type_info(string); +} + +// Widgets that need a GetRect id get one per field. Two autoedit panels can be +// on screen at once (the world config and an entity's fields), so the id is +// namespaced by the caller's identifier to keep the two sets apart. +autoedit_widget_id :: (identifier: s32, field_index: int) -> s32 { + return identifier * 1000 + cast(s32) field_index; +} + +// A text input hands back a view into the widget's own buffer, so the value has +// to be copied out on every keystroke. The previous copy is freed only when this +// same helper allocated it and the field still points at it: a field's starting +// value is usually a literal from the struct definition, which must not be freed. +autoedit_owned_text : Table(*string, string); + +autoedit_set_text :: (field: *string, text: string) -> string { + found, previous := table_find(*autoedit_owned_text, field); + if found && previous.data == field.data free(previous); + result := copy_string(text); + table_set(*autoedit_owned_text, field, result); + return result; +} + +input_code_from_type_and_notes :: (name: string, type: *Type_Info, notes: []string, index: int) -> string { autoconf := note_to_autoedit_conf(notes); builder : String_Builder; + // Each field resets the row height, so a field that wants a taller widget can + // just set it and let the trailing advance pick it up. + print_to_builder(*builder, "r.h = ui_h(4,0);\n"); print_to_builder(*builder, "GR.label(r, \"%\", *t_label_left(theme));\n", name); print_to_builder(*builder, "r.y += r.h;\n"); - - if type == type_info(float) || type == type_info(s32) { + + if type == type_info(bool) { + print_to_builder(*builder, "if GR.button(r, ifx value.% then \"true\" else \"false\", *t_button_selectable(theme, value.%), autoedit_widget_id(identifier, %)) then value.% = !value.%;\n", + name, name, index, name, name); + } else if type == type_info(float) || type == type_info(s32) { if autoconf.kind == .SLIDER { print_to_builder(*builder, "GR.slider(r, *value.%, %, %, %, *theme.slider_theme);\n", name, autoconf.min, autoconf.max, autoconf.step); } else { print_to_builder(*builder, "GR.number_input(r, tprint(\"\%\", value.%), *value.%, %, %, *number_theme);\n", name, name, autoconf.min, autoconf.max); } + } else if type == type_info(string) { + // Roomier than the other widgets: text fields hold a sentence, not a number. + print_to_builder(*builder, "r.h = ui_h(7,0);\n"); + print_to_builder(*builder, "{\n"); + print_to_builder(*builder, "action, _, text_state := GR.text_input(r, value.%, *theme.text_input_theme, autoedit_widget_id(identifier, %));\n", name, index); + print_to_builder(*builder, "if (action & .TEXT_MODIFIED) || (action & .ENTERED) value.% = autoedit_set_text(*value.%, text_state.text);\n", name, name); + print_to_builder(*builder, "}\n"); } else if type == type_info(Vector3) { if autoconf.kind == .DEFAULT { print_to_builder(*builder, "{\n"); @@ -83,9 +124,9 @@ input_code_from_type_and_notes :: (name: string, type: *Type_Info, notes: []stri print_to_builder(*builder, "r.w = orig_w; r.x = orig_x;\n"); print_to_builder(*builder, "}\n"); } else if autoconf.kind == .COLOR { - print_to_builder(*builder, "if GR.button(r, \"Edit color\", *t_button_color(theme, .{value.%.x, value.%.y, value.%.z, 1.0})) then cur_edit_color = *value.%;\n", name, name, name, name); + print_to_builder(*builder, "if GR.button(r, \"Edit color\", *t_button_color(theme, .{value.%.x, value.%.y, value.%.z, 1.0}), autoedit_widget_id(identifier, %)) then cur_edit_color = *value.%;\n", name, name, name, index, name); } - + } print_to_builder(*builder, "r.y += r.h;\n"); return builder_to_string(*builder); @@ -106,7 +147,10 @@ autoedit :: (rect: GR.Rect, value: *$T, theme: *GR.Overall_Theme, identifier: s3 ti := type_info(T); #assert #run type_info(T).type == .STRUCT "Autoedit only works for structs"; for ti.members { - print_to_builder(*builder, "%\n", input_code_from_type_and_notes(it.name, it.type, it.notes)); + if it.flags & .CONSTANT continue; + if it.flags & .USING continue; // e.g. an entity's Entity base + if !autoedit_supports_type(it.type) continue; + print_to_builder(*builder, "%\n", input_code_from_type_and_notes(it.name, it.type, it.notes, it_index)); } return builder_to_string(*builder); } diff --git a/src/world.jai b/src/world.jai index b066399..bdb0507 100644 --- a/src/world.jai +++ b/src/world.jai @@ -91,6 +91,8 @@ World :: struct { chunks : Table(Chunk_Key, Chunk, chunk_key_hash, chunk_key_compare); emitter_instances : [..]Particle_Emitter_Instance; notes : [..]Editor_Note; + entities : [..]*Entity; + next_entity_id : u32; rdm_overrides : [..]Rdm_Instance_Override; rdm_lookup : [..]Rdm_Atlas_Entry; // populated by bake (Step 5) and by loader (Step 6) } @@ -207,6 +209,33 @@ lworld :: (name: string) { load_world(name); } @Command; +// Copy another world's config (sky, sun, water, ...) onto the loaded world, so a +// new level can start from an existing look instead of retyping the values. +copy_world_info :: (name: string) { + if !current_world.valid { + log_error("Cannot copy: no world loaded"); + return; + } + #if OS != .WASM { + file :: #import "File"; + path := tprint("%/worlds/%/world.json", GAME_RESOURCES_DIR, name); + json_str, read_ok := file.read_entire_file(path,, allocator = temp); + if !read_ok { + log_error("Cannot copy: failed to read '%'", path); + return; + } + + parse_ok, wj := Jaison.json_parse_string(json_str, World_Json,, temp); + if !parse_ok { + log_error("Cannot copy: failed to parse world '%'", name); + return; + } + + current_world.world.conf = world_config_from_json(*wj.config); + log_info("Copied world config from '%'", name); + } +} @Command + init_world_system :: () { Pool.set_allocators(*current_world.pool); } @@ -222,6 +251,10 @@ unload_current_world :: () { array_free(chunk.groups); } deinit(*current_world.world.chunks); + free_all_entities(*current_world.world); + // Entity ids restart per world, so a stale editor selection would latch onto + // an unrelated entity in the next one. + #if OS != .WASM { level_editor_clear_entity_selection(); } array_free(current_world.world.rdm_lookup); Pool.reset(*current_world.pool); current_world.valid = false; @@ -353,6 +386,11 @@ resolve_emitter_definitions :: (world: *World) { for *inst: world.emitter_instances { inst.definition = get_emitter_def(inst.definition_name); } + for e: world.entities { + for *inst: e.emitters { + inst.definition = get_emitter_def(inst.definition_name); + } + } } clear_world :: () { @@ -384,6 +422,7 @@ World_Json :: struct { chunks : [..]World_Json_Chunk; emitters : [..]World_Json_Emitter; notes : [..]World_Json_Note; + entities : [..]World_Json_Entity; rdm_overrides : [..]World_Json_Rdm_Override; } @@ -424,6 +463,17 @@ World_Json_Note :: struct { position : [3]s32; } +// Entity fields are stored as name/value string pairs rather than a typed +// object, so that adding, removing or renaming a field never breaks loading: +// unknown names are skipped and missing ones keep their struct defaults. +World_Json_Entity :: struct { + type : string; // entity type name, so reordering ENTITY_TYPES is safe + id : u32; + position : [3]float; + orientation : u8; // absent in worlds saved before entities could rotate + fields : [..]Entity_Field; +} + World_Json_Rdm_Override :: struct { x : s32; y : s32; @@ -619,7 +669,7 @@ save_world :: (world: *World) -> (json: string, chunks_bin: string) { } wj: World_Json; - wj.version = 4; + wj.version = 5; wj.name = world.name; wj.config = world_config_to_json(*world.conf); @@ -648,6 +698,16 @@ save_world :: (world: *World) -> (json: string, chunks_bin: string) { array_add(*wj.notes, jn); } + for e: world.entities { + je: World_Json_Entity; + je.type = entity_type_name(e.type); + je.id = e.id; + je.position = e.position.component; + je.orientation = e.orientation; + for f: entity_fields_to_strings(e) array_add(*je.fields, f); + array_add(*wj.entities, je); + } + for ov: world.rdm_overrides { size := ifx ov.rdm_size > 0 then ov.rdm_size else RDM_DEFAULT_SIZE; array_add(*wj.rdm_overrides, .{x=ov.x, y=ov.y, z=ov.z, rdm_enabled=ov.rdm_enabled, rdm_size=size}); @@ -735,6 +795,16 @@ load_world_from_json :: (json_str: string, chunk_bin: []u8) -> (World, bool) { array_add(*world.notes, note); } + for je: wj.entities { + e := spawn_entity(*world, je.type, .{je.position[0], je.position[1], je.position[2]}, je.orientation); + if e == null then continue; // unknown type; spawn_entity logged it + e.id = je.id; + if je.id >= world.next_entity_id then world.next_entity_id = je.id + 1; + for f: je.fields { + entity_apply_field(e, f.name, f.value); + } + } + for jov: wj.rdm_overrides { size := ifx jov.rdm_size > 0 then jov.rdm_size else RDM_DEFAULT_SIZE; array_add(*world.rdm_overrides, .{x=jov.x, y=jov.y, z=jov.z, rdm_enabled=jov.rdm_enabled, rdm_size=size}); diff --git a/test_game/game.jai b/test_game/game.jai index ced4652..9fec87d 100644 --- a/test_game/game.jai +++ b/test_game/game.jai @@ -1,3 +1,11 @@ +// @Entity is what registers a type with the engine; see src/entities.jai. +Test_Marker :: struct { + #as using base : Entity; + base.type = Test_Marker; + + PARTS :: Entity_Part.[]; +} @Entity + game_engine_config :: () { } diff --git a/test_game/resources/game_core/sprites/anim.sheet.json b/test_game/resources/game_core/sprites/anim.sheet.json deleted file mode 100644 index 8db367f..0000000 --- a/test_game/resources/game_core/sprites/anim.sheet.json +++ /dev/null @@ -1,702 +0,0 @@ -{ "frames": [ - { - "filename": "animsheet 0.aseprite", - "frame": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 120 - }, - { - "filename": "animsheet 1.aseprite", - "frame": { "x": 50, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 2.aseprite", - "frame": { "x": 100, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 3.aseprite", - "frame": { "x": 150, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 4.aseprite", - "frame": { "x": 200, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 5.aseprite", - "frame": { "x": 250, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 6.aseprite", - "frame": { "x": 300, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 120 - }, - { - "filename": "animsheet 7.aseprite", - "frame": { "x": 350, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 90 - }, - { - "filename": "animsheet 8.aseprite", - "frame": { "x": 400, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 9.aseprite", - "frame": { "x": 450, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 10.aseprite", - "frame": { "x": 500, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 11.aseprite", - "frame": { "x": 550, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 12.aseprite", - "frame": { "x": 600, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 13.aseprite", - "frame": { "x": 650, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 14.aseprite", - "frame": { "x": 700, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 90 - }, - { - "filename": "animsheet 15.aseprite", - "frame": { "x": 750, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 16.aseprite", - "frame": { "x": 800, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 17.aseprite", - "frame": { "x": 850, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 18.aseprite", - "frame": { "x": 900, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 19.aseprite", - "frame": { "x": 950, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 20.aseprite", - "frame": { "x": 1000, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 21.aseprite", - "frame": { "x": 1050, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 22.aseprite", - "frame": { "x": 1100, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 23.aseprite", - "frame": { "x": 1150, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 24.aseprite", - "frame": { "x": 1200, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 25.aseprite", - "frame": { "x": 1250, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 26.aseprite", - "frame": { "x": 1300, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 27.aseprite", - "frame": { "x": 1350, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 28.aseprite", - "frame": { "x": 1400, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 29.aseprite", - "frame": { "x": 1450, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 30.aseprite", - "frame": { "x": 1500, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 31.aseprite", - "frame": { "x": 1550, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 32.aseprite", - "frame": { "x": 1600, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 33.aseprite", - "frame": { "x": 1650, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 34.aseprite", - "frame": { "x": 1700, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 35.aseprite", - "frame": { "x": 1750, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 36.aseprite", - "frame": { "x": 1800, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 37.aseprite", - "frame": { "x": 1850, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 120 - }, - { - "filename": "animsheet 38.aseprite", - "frame": { "x": 1900, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 39.aseprite", - "frame": { "x": 1950, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 40.aseprite", - "frame": { "x": 2000, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 41.aseprite", - "frame": { "x": 2050, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 42.aseprite", - "frame": { "x": 2100, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 43.aseprite", - "frame": { "x": 2150, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 120 - }, - { - "filename": "animsheet 44.aseprite", - "frame": { "x": 2200, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 90 - }, - { - "filename": "animsheet 45.aseprite", - "frame": { "x": 2250, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 46.aseprite", - "frame": { "x": 2300, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 47.aseprite", - "frame": { "x": 2350, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 48.aseprite", - "frame": { "x": 2400, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 49.aseprite", - "frame": { "x": 2450, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 50.aseprite", - "frame": { "x": 2500, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 51.aseprite", - "frame": { "x": 2550, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 90 - }, - { - "filename": "animsheet 52.aseprite", - "frame": { "x": 2600, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 53.aseprite", - "frame": { "x": 2650, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 54.aseprite", - "frame": { "x": 2700, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 55.aseprite", - "frame": { "x": 2750, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 56.aseprite", - "frame": { "x": 2800, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 57.aseprite", - "frame": { "x": 2850, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 58.aseprite", - "frame": { "x": 2900, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 59.aseprite", - "frame": { "x": 2950, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 60.aseprite", - "frame": { "x": 3000, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 61.aseprite", - "frame": { "x": 3050, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 62.aseprite", - "frame": { "x": 3100, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 63.aseprite", - "frame": { "x": 3150, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 64.aseprite", - "frame": { "x": 3200, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 65.aseprite", - "frame": { "x": 3250, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 66.aseprite", - "frame": { "x": 3300, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 67.aseprite", - "frame": { "x": 3350, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 68.aseprite", - "frame": { "x": 3400, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 69.aseprite", - "frame": { "x": 3450, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 70.aseprite", - "frame": { "x": 3500, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 71.aseprite", - "frame": { "x": 3550, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 72.aseprite", - "frame": { "x": 3600, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 73.aseprite", - "frame": { "x": 3650, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - } - ], - "meta": { - "app": "http://www.aseprite.org/", - "version": "1.3.15.5-x64", - "image": "animsheet.png", - "format": "RGBA8888", - "size": { "w": 3700, "h": 48 }, - "scale": "1", - "frameTags": [ - { "name": "player_idle", "from": 0, "to": 6, "direction": "forward", "color": "#000000ff" }, - { "name": "player_walk", "from": 7, "to": 14, "direction": "forward", "color": "#000000ff" }, - { "name": "player_run", "from": 15, "to": 22, "direction": "forward", "color": "#000000ff" }, - { "name": "player_stop", "from": 23, "to": 24, "direction": "forward", "color": "#000000ff" }, - { "name": "ball", "from": 27, "to": 27, "direction": "forward", "color": "#000000ff" }, - { "name": "jump_up", "from": 28, "to": 28, "direction": "forward", "color": "#000000ff" }, - { "name": "jump_top", "from": 29, "to": 29, "direction": "forward", "color": "#000000ff" }, - { "name": "jump_down", "from": 30, "to": 30, "direction": "forward", "color": "#000000ff" }, - { "name": "jump_land", "from": 31, "to": 32, "direction": "forward", "color": "#000000ff" }, - { "name": "slide", "from": 33, "to": 36, "direction": "forward", "color": "#000000ff" }, - { "name": "red_idle", "from": 37, "to": 43, "direction": "forward", "color": "#000000ff" }, - { "name": "red_walk", "from": 44, "to": 51, "direction": "forward", "color": "#000000ff" }, - { "name": "red_run", "from": 52, "to": 59, "direction": "forward", "color": "#000000ff" }, - { "name": "red_jump_up", "from": 65, "to": 65, "direction": "forward", "color": "#000000ff" }, - { "name": "red_jump_top", "from": 66, "to": 66, "direction": "forward", "color": "#000000ff" }, - { "name": "red_jump_down", "from": 67, "to": 67, "direction": "forward", "color": "#000000ff" }, - { "name": "red_jump_land", "from": 68, "to": 69, "direction": "forward", "color": "#000000ff" }, - { "name": "red_slide", "from": 70, "to": 73, "direction": "forward", "color": "#000000ff" } - ], - "layers": [ - { "name": "Layer 1", "opacity": 255, "blendMode": "normal" } - ], - "slices": [ - ] - } -} diff --git a/test_game/resources/game_core/sprites/anim.sheet.png b/test_game/resources/game_core/sprites/anim.sheet.png deleted file mode 100644 index 9a31876..0000000 Binary files a/test_game/resources/game_core/sprites/anim.sheet.png and /dev/null differ diff --git a/test_game/resources/game_core/sprites/score.sheet.json b/test_game/resources/game_core/sprites/score.sheet.json deleted file mode 100644 index f15d006..0000000 --- a/test_game/resources/game_core/sprites/score.sheet.json +++ /dev/null @@ -1,267 +0,0 @@ -{ "frames": [ - { - "filename": "scoresheet 0.aseprite", - "frame": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 1.aseprite", - "frame": { "x": 96, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 2.aseprite", - "frame": { "x": 192, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 3.aseprite", - "frame": { "x": 288, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 4.aseprite", - "frame": { "x": 384, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 5.aseprite", - "frame": { "x": 480, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 6.aseprite", - "frame": { "x": 576, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 7.aseprite", - "frame": { "x": 672, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 8.aseprite", - "frame": { "x": 768, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 9.aseprite", - "frame": { "x": 864, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 10.aseprite", - "frame": { "x": 960, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 11.aseprite", - "frame": { "x": 1056, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 12.aseprite", - "frame": { "x": 1152, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 13.aseprite", - "frame": { "x": 1248, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 14.aseprite", - "frame": { "x": 1344, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 15.aseprite", - "frame": { "x": 1440, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 16.aseprite", - "frame": { "x": 1536, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 17.aseprite", - "frame": { "x": 1632, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 18.aseprite", - "frame": { "x": 1728, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 19.aseprite", - "frame": { "x": 1824, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 20.aseprite", - "frame": { "x": 1920, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 21.aseprite", - "frame": { "x": 2016, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 22.aseprite", - "frame": { "x": 2112, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 23.aseprite", - "frame": { "x": 2208, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 24.aseprite", - "frame": { "x": 2304, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - } - ], - "meta": { - "app": "http://www.aseprite.org/", - "version": "1.3.16.1-x64", - "format": "RGBA8888", - "size": { "w": 2400, "h": 48 }, - "scale": "1", - "frameTags": [ - { "name": "lscore_0", "from": 0, "to": 0, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_1", "from": 1, "to": 1, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_2", "from": 2, "to": 2, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_3", "from": 3, "to": 3, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_4", "from": 4, "to": 4, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_5", "from": 5, "to": 5, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_6", "from": 6, "to": 6, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_7", "from": 7, "to": 7, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_8", "from": 8, "to": 8, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_adv", "from": 9, "to": 9, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_0", "from": 10, "to": 10, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_1", "from": 11, "to": 11, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_2", "from": 12, "to": 12, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_3", "from": 13, "to": 13, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_4", "from": 14, "to": 14, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_5", "from": 15, "to": 15, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_6", "from": 16, "to": 16, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_7", "from": 17, "to": 17, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_8", "from": 18, "to": 18, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_adv", "from": 19, "to": 19, "direction": "forward", "color": "#000000ff" }, - { "name": "score_middle", "from": 20, "to": 20, "direction": "forward", "color": "#000000ff" }, - { "name": "score_board", "from": 21, "to": 21, "direction": "forward", "color": "#000000ff" }, - { "name": "score_board_l", "from": 22, "to": 22, "direction": "forward", "color": "#000000ff" }, - { "name": "score_board_r", "from": 23, "to": 23, "direction": "forward", "color": "#000000ff" }, - { "name": "logo", "from": 24, "to": 24, "direction": "forward", "color": "#000000ff" } - ], - "layers": [ - { "name": "Layer 1", "opacity": 255, "blendMode": "normal" } - ], - "slices": [ - ] - } -} diff --git a/test_game/resources/game_core/sprites/score.sheet.png b/test_game/resources/game_core/sprites/score.sheet.png deleted file mode 100644 index f18cd77..0000000 Binary files a/test_game/resources/game_core/sprites/score.sheet.png and /dev/null differ diff --git a/test_game/resources/worlds/test_world/world.json b/test_game/resources/worlds/test_world/world.json index 725a269..4bf7754 100644 --- a/test_game/resources/worlds/test_world/world.json +++ b/test_game/resources/worlds/test_world/world.json @@ -1,5 +1,5 @@ { - "version": 4, + "version": 5, "name": "test_world", "config": { "skyBase": [ @@ -23,8 +23,8 @@ "sunPosition": [ 0.371391,0.557086,0.742781 ], - "sunIntensity": 2, - "skyIntensity": 1, + "sunIntensity": 1, + "skyIntensity": 0.3, "hasClouds": 1, "planeHeight": 0, "animatePlaneHeight": 1, @@ -33,12 +33,17 @@ ], "deepColor": [ 1,1,1 - ] + ], + "waterShininess": 64 }, "chunks": [ ], "emitters": [ ], "notes": [ + ], + "entities": [ + ], + "rdm_overrides": [ ] } \ No newline at end of file diff --git a/test_packs/game_core.pack b/test_packs/game_core.pack index 5f84daa..79cf58e 100644 Binary files a/test_packs/game_core.pack and b/test_packs/game_core.pack differ