add entity code

This commit is contained in:
Tuomas Katajisto 2026-08-02 11:42:00 +03:00
parent 69e3dd26dc
commit cba645adcf
35 changed files with 2680 additions and 1143 deletions

142
run_tests.sh Executable file
View File

@ -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."

View File

@ -2,6 +2,7 @@
#load "iprof.jai"; #load "iprof.jai";
#load "trile_thumbnails.jai"; #load "trile_thumbnails.jai";
#load "picker.jai"; #load "picker.jai";
#load "gizmo.jai";
#load "trile_editor.jai"; #load "trile_editor.jai";
#load "level_editor.jai"; #load "level_editor.jai";
#load "particle_editor.jai"; #load "particle_editor.jai";

455
src/editor/gizmo.jai Normal file
View File

@ -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();
}
}

View File

@ -29,6 +29,7 @@ Level_Editor_Tool_Mode :: enum {
LINE; LINE;
INSPECTOR; INSPECTOR;
VIEWER; VIEWER;
ENTITY;
} }
current_tool_mode : Level_Editor_Tool_Mode = .POINT; current_tool_mode : Level_Editor_Tool_Mode = .POINT;
@ -59,6 +60,21 @@ inspector_rdm_roughness : int = 0;
inspector_note_deactivate : bool; inspector_note_deactivate : bool;
inspector_note_activate_next : 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; editor_billboards_visible : bool = true;
get_current_orientation :: () -> u8 { get_current_orientation :: () -> u8 {
@ -333,6 +349,8 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
r.y += r.h; 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; 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; 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; 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; 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)); GR.label(r, "Click start of line", *t_label_left(theme));
} }
r.y += r.h; 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); r.h = ui_h(3, 2);
if GR.button(r, ifx editor_billboards_visible then "Hide Markers" else "Show Markers", *theme.button_theme, 199) { if GR.button(r, ifx editor_billboards_visible then "Hide Markers" else "Show Markers", *theme.button_theme, 199) {
editor_billboards_visible = !editor_billboards_visible; 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 { editor_edit_y :: () -> float {
y := cast(float)editY; y := cast(float)editY;
curworld := get_current_world(); curworld := get_current_world();
@ -525,6 +658,9 @@ tick_level_editor :: () {
tick_level_editor_camera(); tick_level_editor_camera();
tick_particles(cast(float)delta_time); 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) { if is_action_start(Editor_Action.LEVEL_TWIST_CCW) {
lastInputTime = get_time(); lastInputTime = get_time();
current_orientation_twist = (current_orientation_twist + 1) % 4; current_orientation_twist = (current_orientation_twist + 1) % 4;
@ -538,9 +674,14 @@ tick_level_editor :: () {
current_orientation_face = (current_orientation_face + 1) % 6; 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); 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; show_trile_preview = false;
if hit { if hit {
show_trile_preview = true; show_trile_preview = true;
@ -632,6 +773,12 @@ tick_level_editor :: () {
inspector_z = cast(s32)pz; inspector_z = cast(s32)pz;
inspector_note_deactivate = true; 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; 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 { if current_tool_mode == .INSPECTOR {
name, orientation, found := get_trile_at(*curworld.world, xx px, xx py, xx pz); 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 ""); 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 :: () { add_editor_billboards :: () {
if !editor_billboards_visible then return; if !editor_billboards_visible then return;
curworld := get_current_world(); curworld := get_current_world();
if !curworld.valid then return; 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"); anim := get_animation_from_string("game_core.ball");
if anim == null then return; if anim == null then return;
@ -1012,6 +1282,7 @@ draw_level_editor :: () {
ph := effective_plane_height(*curworld.world.conf); ph := effective_plane_height(*curworld.world.conf);
create_set_cam_rendering_task(cam, ph); create_set_cam_rendering_task(cam, ph);
create_world_rendering_tasks(*curworld.world, cam, ph); create_world_rendering_tasks(*curworld.world, cam, ph);
add_entity_render_tasks(*curworld.world);
if show_trile_preview && !trile_preview_disabled { if show_trile_preview && !trile_preview_disabled {
create_level_editor_preview_tasks(); create_level_editor_preview_tasks();
} }
@ -1043,12 +1314,14 @@ draw_level_editor_ui :: (theme: *GR.Overall_Theme) {
case .INFO; case .INFO;
if curworld.valid then autoedit(r, *curworld.world.conf, theme); 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)); 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); draw_bg_rectangle(rr, theme);
ui_add_mouse_occluder(rr); ui_add_mouse_occluder(rr);
rr.y += ui_h(1, 0); 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); draw_inspector_panel(*rr, theme);
} else { } else {
rr.h = ui_h(3, 2); rr.h = ui_h(3, 2);

408
src/entities.jai Normal file
View File

@ -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);
}

View File

@ -28,6 +28,7 @@ Editor_Action :: enum {
LEVEL_TOOL_LINE; LEVEL_TOOL_LINE;
LEVEL_TOOL_INSPECTOR; LEVEL_TOOL_INSPECTOR;
LEVEL_TOOL_VIEWER; LEVEL_TOOL_VIEWER;
LEVEL_TOOL_ENTITY;
// Level editor — tabs // Level editor — tabs
LEVEL_TAB_TOOLS; LEVEL_TAB_TOOLS;
LEVEL_TAB_INFO; LEVEL_TAB_INFO;
@ -180,6 +181,7 @@ set_default_bindings :: () {
set(.LEVEL_TOOL_LINE, cast(Key_Code) #char "4"); set(.LEVEL_TOOL_LINE, cast(Key_Code) #char "4");
set(.LEVEL_TOOL_INSPECTOR, cast(Key_Code) #char "5"); set(.LEVEL_TOOL_INSPECTOR, cast(Key_Code) #char "5");
set(.LEVEL_TOOL_VIEWER, cast(Key_Code) #char "6"); 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_TOOLS, cast(Key_Code) #char "T");
set(.LEVEL_TAB_INFO, cast(Key_Code) #char "I"); set(.LEVEL_TAB_INFO, cast(Key_Code) #char "I");
set(.LEVEL_TAB_TACOMA, cast(Key_Code) #char "X"); set(.LEVEL_TAB_TACOMA, cast(Key_Code) #char "X");

View File

@ -1,4 +1,4 @@
#if FLAG_TEST_EXE_ENGINE { #if FLAG_USE_TEST_GAME {
PACK_DIR :: "./test_packs"; PACK_DIR :: "./test_packs";
GAME_RESOURCES_DIR :: "./test_game/resources"; GAME_RESOURCES_DIR :: "./test_game/resources";
} else { } else {

View File

@ -18,8 +18,10 @@ _emit :: (level: Log_Level, message: string) {
else "[INFO] "; else "[INFO] ";
// We need to protect the logger from custom allocators since we don't // We need to protect the logger from custom allocators since we don't
// want those meddling with our log row allocation! // want those meddling with our log row allocation! Before the engine has
push_allocator(default_allocator); // 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)); line := copy_string(tprint("%1%2", prefix, message));
print("%\n", line); print("%\n", line);
console_add_output_line(line); console_add_output_line(line);

View File

@ -15,6 +15,17 @@ String :: #import "String";
Jaison :: #import "Jaison"; Jaison :: #import "Jaison";
stbi :: #import "stb_image"; 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"; } #if (FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE) { #load "tests/index.jai"; }
#load "logging.jai"; #load "logging.jai";
@ -32,6 +43,8 @@ stbi :: #import "stb_image";
#load "ray.jai"; #load "ray.jai";
#load "profiling.jai"; #load "profiling.jai";
#load "particles/particles.jai"; #load "particles/particles.jai";
#load "orientation.jai";
#load "entities.jai";
#load "world.jai"; #load "world.jai";
#load "utils.jai"; #load "utils.jai";
#load "audio/audio.jai"; #load "audio/audio.jai";
@ -40,10 +53,10 @@ stbi :: #import "stb_image";
#load "loading_screen.jai"; #load "loading_screen.jai";
#load "ui/demo.jai"; #load "ui/demo.jai";
#if !FLAG_TEST_EXE_ENGINE { #if FLAG_USE_TEST_GAME {
#load "../game/game.jai";
} else {
#load "../test_game/game.jai"; #load "../test_game/game.jai";
} else {
#load "../game/game.jai";
} }
last_frame_time : float64; // timestamp of the last frame last_frame_time : float64; // timestamp of the last frame
@ -139,6 +152,10 @@ init :: () {
#if FLAG_TEST_EXE_ENGINE { #if FLAG_TEST_EXE_ENGINE {
engine_exe_tests_add(); 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 :: () { init_after_core :: () {
@ -225,7 +242,7 @@ frame :: () {
add_frame_profiling_point("After loading logic"); add_frame_profiling_point("After loading logic");
#if FLAG_TEST_EXE_ENGINE { #if FLAG_ANY_EXE_TEST {
run_exe_tests(); run_exe_tests();
} }

98
src/meta/entity_types.jai Normal file
View File

@ -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);
}

View File

@ -1,8 +1,10 @@
#load "ascii.jai"; #load "ascii.jai";
#load "../resource_mirror.jai";
#load "pack.jai"; #load "pack.jai";
#load "lint.jai"; #load "lint.jai";
#load "hacks.jai"; #load "hacks.jai";
#load "console_commands.jai"; #load "console_commands.jai";
#load "entity_types.jai";
#load "shaderload.jai"; #load "shaderload.jai";
endframe_modified := false; endframe_modified := false;
@ -14,4 +16,6 @@ custom_message_handler :: (message: *Message, w: *Workspace) {
remove_getrect_custom_cursor(message, w); remove_getrect_custom_cursor(message, w);
add_console_commands(message, w); add_console_commands(message, w);
add_entity_types(message, w);
} }

View File

@ -3,11 +3,9 @@
should_ignore_file :: (name: string) -> bool { should_ignore_file :: (name: string) -> bool {
if name.count > 0 && name[0] == #char "." return true; if name.count > 0 && name[0] == #char "." return true;
ok, left, right := split_from_right(name, #char "."); // Aseprite sources are inputs (see resource_mirror.jai), not shippable
if right == "aseprite" { // assets; only the .sheet.png/.sheet.json they export go into the pack.
print("Ignoring % as aseprite file...\n", name); if is_aseprite_source(name) return true;
return true;
}
return false; return false;
} }
@ -53,11 +51,10 @@ create_pack :: (include_test_resources: bool = false, pack_dir: string = "./pack
#import "Simple_Package"; #import "Simple_Package";
#import "File"; #import "File";
util.visit_files("./resources", true, true, file_visit_handler); // Packs come from the processed copy of the resource trees, never the trees
if include_test_resources { // themselves. See resource_mirror.jai.
util.visit_files("./test_game/resources", true, true, file_visit_handler); for prepare_resource_mirror(include_test_resources) {
} else { util.visit_files(it, true, true, file_visit_handler);
util.visit_files("./game/resources", true, true, file_visit_handler);
} }
make_directory_if_it_does_not_exist(pack_dir); make_directory_if_it_does_not_exist(pack_dir);

291
src/orientation.jai Normal file
View File

@ -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();
}
}

View File

@ -1,5 +1,7 @@
#if !FLAG_RELEASE_BUILD && OS != .WASM { #if !FLAG_RELEASE_BUILD && OS != .WASM {
#load "resource_mirror.jai";
#import "String"; #import "String";
Pack_Writer :: #import "Simple_Package"; Pack_Writer :: #import "Simple_Package";
File_Util :: #import "File_Utilities"; 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 :: () { recreate_packs_on_disk :: () {
packs: Table(string, Pack_Writer.Create_Package); packs: Table(string, Pack_Writer.Create_Package);
File_Util.visit_files("./resources", true, *packs, _hotreload_visitor); // Same mirror-and-process pipeline the build uses, so a hot reload picks up
File_Util.visit_files(GAME_RESOURCES_DIR, true, *packs, _hotreload_visitor); // 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 { for pack, key: packs {
Pack_Writer.write(*pack, tprint("%/%.pack", PACK_DIR, key)); Pack_Writer.write(*pack, tprint("%/%.pack", PACK_DIR, key));
log_info("Hot-reload: wrote pack '%'", key); log_info("Hot-reload: wrote pack '%'", key);

View File

@ -17,6 +17,7 @@ Render_Command_Type :: enum {
SET_CAMERA; SET_CAMERA;
DRAW_GROUND; DRAW_GROUND;
ADD_TRILE_POSITIONS; ADD_TRILE_POSITIONS;
ADD_TRILE_POSITIONS_F32;
DRAW_TRILE_POSITIONS; DRAW_TRILE_POSITIONS;
ADD_TRILE_RDM_POSITION; ADD_TRILE_RDM_POSITION;
DRAW_TRILE_RDM; DRAW_TRILE_RDM;
@ -51,6 +52,15 @@ Render_Command_Add_Trile_Positions :: struct {
chunk : Chunk_Key; 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 { Render_Command_Draw_Trile_Positions :: struct {
#as using c : Render_Command; #as using c : Render_Command;
c.type = .DRAW_TRILE_POSITIONS; c.type = .DRAW_TRILE_POSITIONS;

View File

@ -22,6 +22,9 @@ backend_handle_command :: (cmd: *Render_Command) {
case .ADD_TRILE_POSITIONS; case .ADD_TRILE_POSITIONS;
add_command := cast(*Render_Command_Add_Trile_Positions)cmd; add_command := cast(*Render_Command_Add_Trile_Positions)cmd;
backend_add_trile_positions(add_command.positions, add_command.chunk); 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; case .DRAW_TRILE_POSITIONS;
draw_command := cast(*Render_Command_Draw_Trile_Positions)cmd; 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); 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); 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) { 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 { if in_gbuffer_pass {
backend_draw_trile_positions_gbuffer(trile, amount, worldConf, offset_index, lod_index); backend_draw_trile_positions_gbuffer(trile, amount, worldConf, offset_index, lod_index);

View File

@ -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; 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_enabled : bool = !FLAG_RELEASE_BUILD;
debug_draw_grid : bool = true; debug_draw_grid : bool = true;
debug_draw_vectors : bool = true; debug_draw_vectors : bool = true;
@ -24,27 +42,35 @@ toggle_debug_colliders :: () {
debug_draw_colliders = !debug_draw_colliders; debug_draw_colliders = !debug_draw_colliders;
} @Command } @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 !debug_draw_enabled then return;
if g_debug_line_count >= DEBUG_LINE_MAX then return; if g_debug_line_count >= DEBUG_LINE_MAX then return;
base := g_debug_line_count * 14; g_debug_lines[g_debug_line_count] = .{a, b, col, width};
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_line_count += 1; 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) { debug_vector :: (origin: Vector3, vec: Vector3, col: Vector4) {
if !debug_draw_enabled || !debug_draw_vectors then return; if !debug_draw_enabled || !debug_draw_vectors then return;
tip := origin + vec; 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_line(.{mx.x, mn.y, mx.z}, .{mx.x, mx.y, mx.z}, col);
} }
debug_draw_flush_gpu :: () { flush_debug_lines :: (pipe: *Pipeline_Binding, lines: []Debug_Line, count: *int, params: *Debugline_Vs_Params) {
if g_debug_line_count == 0 then return; if count.* == 0 then return;
// Buffer 0 is the shared static quad; buffer 1 is this pipeline's instances.
sg_update_buffer( sg_update_buffer(
gPipelines.debugline.bind.vertex_buffers[0], pipe.bind.vertex_buffers[1],
*(sg_range.{ *(sg_range.{
ptr = g_debug_line_verts.data, ptr = lines.data,
size = cast(u64)(g_debug_line_count * 14 * size_of(float)), 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 : Debugline_Vs_Params;
params.mvp = mvp.floats; params.mvp = mvp.floats;
sg_apply_pipeline(gPipelines.debugline.pipeline); params.viewport = .[cast(float)w, cast(float)h, 0, 0];
sg_apply_bindings(*gPipelines.debugline.bind);
sg_apply_uniforms(UB_debugline_vs_params, *(sg_range.{ ptr = *params, size = size_of(Debugline_Vs_Params) })); flush_debug_lines(*gPipelines.debugline, g_debug_lines, *g_debug_line_count, *params);
sg_draw(0, xx (g_debug_line_count * 2), 1); flush_debug_lines(*gPipelines.debugline_overlay, g_overlay_lines, *g_overlay_line_count, *params);
g_debug_line_count = 0;
} }
#assert size_of(Debug_Line) == 44 "Debug_Line must stay tightly packed: the pipeline layout hardcodes its field offsets.";

View File

@ -113,6 +113,8 @@ gPipelines : struct {
sh_irradiance: Pipeline_Binding; sh_irradiance: Pipeline_Binding;
debugline : Pipeline_Binding; debugline : Pipeline_Binding;
// Same shader, but depth testing disabled so editor gizmos draw on top.
debugline_overlay : Pipeline_Binding;
} }
create_final_image :: () { create_final_image :: () {
@ -1694,20 +1696,45 @@ create_particle_pipeline :: () {
} }
create_debugline_pipeline :: () { create_debugline_pipeline :: () {
buf_desc := sg_buffer_desc.{ // The two corner coordinates of a quad: x picks the side of the line, y picks
size = DEBUG_LINE_MAX * 2 * 7 * size_of(float), // the end. The vertex shader turns those into a screen-space widened line.
usage = .DYNAMIC, corners := float.[
label = "debug_line_verts", -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; pipeline : sg_pipeline_desc;
shader_desc := debugline_shader_desc(sg_query_backend()); shader_desc := debugline_shader_desc(sg_query_backend());
pipeline.shader = sg_make_shader(*shader_desc); pipeline.shader = sg_make_shader(*shader_desc);
pipeline.primitive_type = .LINES; pipeline.layout.buffers[0].stride = 2 * size_of(float);
pipeline.layout.buffers[0].stride = 28; pipeline.layout.buffers[1].stride = size_of(Debug_Line);
pipeline.layout.attrs[ATTR_debugline_a_pos] = .{ format = .FLOAT3, buffer_index = 0, offset = 0 }; pipeline.layout.buffers[1].step_func = .PER_INSTANCE;
pipeline.layout.attrs[ATTR_debugline_a_col] = .{ format = .FLOAT4, buffer_index = 0, offset = 12 }; 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 = .{ pipeline.depth = .{
write_enabled = false, write_enabled = false,
compare = .LESS_EQUAL, compare = .LESS_EQUAL,
@ -1720,4 +1747,13 @@ create_debugline_pipeline :: () {
pipeline.colors[0] = color_state; pipeline.colors[0] = color_state;
pipeline.label = "debugline_pipeline"; pipeline.label = "debugline_pipeline";
gPipelines.debugline.pipeline = sg_make_pipeline(*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);
} }

View File

@ -7,6 +7,7 @@ Rendering_Task_Type :: enum {
SET_CAMERA; SET_CAMERA;
SET_LIGHT; 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; // 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; TRILE_RDM;
TRIXELS; TRIXELS;
BILLBOARD; BILLBOARD;
@ -65,6 +66,19 @@ Rendering_Task_Trile :: struct {
lod_index : s32 = 0; // 0 = full detail, 1 = 4^3 LOD, 2 = 2^3 LOD 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 { Rendering_Task_Trile_RDM :: struct {
#as using t : Rendering_Task; #as using t : Rendering_Task;
t.type = .TRILE_RDM; t.type = .TRILE_RDM;
@ -177,6 +191,22 @@ tasks_to_commands :: () {
array_add(*render_command_buckets.gbuffer, drawPositionsCmd); array_add(*render_command_buckets.gbuffer, drawPositionsCmd);
array_add(*render_command_buckets.shadow, 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; case .TRILE_RDM;
rdmTask := (cast(*Rendering_Task_Trile_RDM)it); rdmTask := (cast(*Rendering_Task_Trile_RDM)it);
addCmd := New(Render_Command_Add_Trile_RDM_Position,, temp); addCmd := New(Render_Command_Add_Trile_RDM_Position,, temp);

162
src/resource_mirror.jai Normal file
View File

@ -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);
}

View File

@ -13,30 +13,55 @@
Vertex Shader: vs_debugline Vertex Shader: vs_debugline
Fragment Shader: fs_debugline Fragment Shader: fs_debugline
Attributes: Attributes:
ATTR_debugline_a_pos => 0 ATTR_debugline_a_corner => 0
ATTR_debugline_a_col => 1 ATTR_debugline_a_pos_a => 1
ATTR_debugline_a_pos_b => 2
ATTR_debugline_a_col => 3
ATTR_debugline_a_width => 4
Bindings: Bindings:
Uniform block 'debugline_vs_params': Uniform block 'debugline_vs_params':
Jai struct: Debugline_Vs_Params Jai struct: Debugline_Vs_Params
Bind slot: UB_debugline_vs_params => 0 Bind slot: UB_debugline_vs_params => 0
*/ */
ATTR_debugline_a_pos :: 0; ATTR_debugline_a_corner :: 0;
ATTR_debugline_a_col :: 1; 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; UB_debugline_vs_params :: 0;
Debugline_Vs_Params :: struct { Debugline_Vs_Params :: struct {
mvp: [16]float; mvp: [16]float;
viewport: [4]float;
}; };
/* /*
#version 430 #version 430
uniform vec4 debugline_vs_params[4]; uniform vec4 debugline_vs_params[5];
layout(location = 0) in vec3 a_pos; 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 = 0) out vec4 v_col;
layout(location = 1) in vec4 a_col; layout(location = 3) in vec4 a_col;
void main() 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; v_col = a_col;
} }
@ -44,26 +69,74 @@ Debugline_Vs_Params :: struct {
vs_debugline_source_glsl430 := u8.[ vs_debugline_source_glsl430 := u8.[
0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x34,0x33,0x30,0x0a,0x0a,0x75,0x6e, 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, 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, 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, 0x6f,0x6e,0x20,0x3d,0x20,0x31,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, 0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,
0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x6f,0x75,0x74,0x20, 0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x32,0x29,0x20,0x69,0x6e,
0x76,0x65,0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x6c,0x61,0x79,0x6f, 0x20,0x76,0x65,0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62,0x3b,0x0a,0x6c,
0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29, 0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,
0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a, 0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x61,0x5f,0x63,0x6f,
0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20, 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,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, 0x20,0x5f,0x31,0x31,0x35,0x20,0x2b,0x20,0x76,0x65,0x63,0x34,0x28,0x28,0x28,0x28,
0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x64,0x65, 0x76,0x65,0x63,0x32,0x28,0x2d,0x5f,0x38,0x36,0x2e,0x79,0x2c,0x20,0x5f,0x38,0x36,
0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d, 0x2e,0x78,0x29,0x20,0x2a,0x20,0x61,0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x78,
0x73,0x5b,0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f, 0x29,0x20,0x2a,0x20,0x28,0x28,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x20,0x2a,0x20,
0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x64,0x65, 0x30,0x2e,0x35,0x29,0x20,0x2b,0x20,0x30,0x2e,0x35,0x29,0x29,0x20,0x2f,0x20,0x64,
0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d, 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,
0x73,0x5b,0x33,0x5d,0x29,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70, 0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x20,0x2a,0x20,0x5f,0x31,0x31,0x35,
0x6f,0x73,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x5f, 0x2e,0x77,0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20,
0x63,0x6f,0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x7d,0x0a,0x0a, 0x20,0x20,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c,
0x00, 0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
]; ];
/* /*
#version 430 #version 430
@ -91,14 +164,32 @@ fs_debugline_source_glsl430 := u8.[
/* /*
#version 300 es #version 300 es
uniform vec4 debugline_vs_params[4]; uniform vec4 debugline_vs_params[5];
layout(location = 0) in vec3 a_pos; 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; out vec4 v_col;
layout(location = 1) in vec4 a_col; layout(location = 3) in vec4 a_col;
void main() 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; 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, 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, 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, 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, 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, 0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,
0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x3b,0x0a,0x6f,0x75,0x74,0x20,0x76,0x65, 0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x3b,0x0a,0x6c,0x61,0x79,0x6f,
0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74, 0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x32,0x29,
0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29,0x20,0x69, 0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62,
0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x0a,0x76, 0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,
0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20, 0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x61,
0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x6d, 0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,
0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73, 0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x34,0x29,0x20,0x69,0x6e,
0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75, 0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x3b,0x0a,
0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b, 0x6f,0x75,0x74,0x20,0x76,0x65,0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,
0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73, 0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,
0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75, 0x3d,0x20,0x33,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61,0x5f,0x63,
0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b, 0x6f,0x6c,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,
0x33,0x5d,0x29,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70,0x6f,0x73, 0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x74,0x34,0x20,0x5f,0x31,0x38,0x20,
0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x5f,0x63,0x6f, 0x3d,0x20,0x6d,0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,
0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x7d,0x0a,0x0a,0x00, 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 #version 300 es
@ -161,6 +301,7 @@ fs_debugline_source_glsl300es := u8.[
struct debugline_vs_params struct debugline_vs_params
{ {
float4x4 mvp; float4x4 mvp;
float4 viewport;
}; };
struct main0_out struct main0_out
@ -171,14 +312,31 @@ fs_debugline_source_glsl300es := u8.[
struct main0_in struct main0_in
{ {
float3 a_pos [[attribute(0)]]; float2 a_corner [[attribute(0)]];
float4 a_col [[attribute(1)]]; 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 = {}; 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; out.v_col = in.a_col;
return out; 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, 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, 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, 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, 0x34,0x20,0x6d,0x76,0x70,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,
0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20, 0x34,0x20,0x76,0x69,0x65,0x77,0x70,0x6f,0x72,0x74,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,
0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x5b, 0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,
0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,0x5d,0x5d,0x3b,0x0a, 0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x76,0x5f,
0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x67,0x6c,0x5f,0x50,0x6f, 0x63,0x6f,0x6c,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,
0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f, 0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,
0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20, 0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,
0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66, 0x73,0x69,0x74,0x69,0x6f,0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,
0x6c,0x6f,0x61,0x74,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x20,0x5b,0x5b,0x61,0x74, 0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,
0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20, 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x61,0x5f,0x63,0x6f,0x72,
0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x20,0x5b, 0x6e,0x65,0x72,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,
0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x31,0x29,0x5d,0x5d,0x3b, 0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x33,
0x0a,0x7d,0x3b,0x0a,0x0a,0x76,0x65,0x72,0x74,0x65,0x78,0x20,0x6d,0x61,0x69,0x6e, 0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,
0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e, 0x62,0x75,0x74,0x65,0x28,0x31,0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,
0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f, 0x6c,0x6f,0x61,0x74,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62,0x20,0x5b,0x5b,
0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,0x64, 0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x32,0x29,0x5d,0x5d,0x3b,0x0a,
0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,
0x6d,0x73,0x26,0x20,0x5f,0x31,0x39,0x20,0x5b,0x5b,0x62,0x75,0x66,0x66,0x65,0x72, 0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x33,0x29,0x5d,
0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x69, 0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x61,0x5f,0x77,
0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,0x20,0x7b,0x7d,0x3b, 0x69,0x64,0x74,0x68,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,
0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69, 0x28,0x34,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x76,0x65,0x72,0x74,0x65,
0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,0x31,0x39,0x2e,0x6d,0x76,0x70,0x20,0x2a, 0x78,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,
0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,0x69,0x6e,0x2e,0x61,0x5f,0x70,0x6f,0x73, 0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,
0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e, 0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,
0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d,0x20,0x69,0x6e,0x2e,0x61,0x5f,0x63,0x6f,0x6c, 0x74,0x61,0x6e,0x74,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,
0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74, 0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x26,0x20,0x5f,0x31,0x33,0x20,0x5b,0x5b,
0x3b,0x0a,0x7d,0x0a,0x0a,0x00, 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 <metal_stdlib> #include <metal_stdlib>
@ -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.source = xx *fs_debugline_source_glsl430;
desc.fragment_func.entry = "main"; desc.fragment_func.entry = "main";
desc.attrs[0].base_type = .FLOAT; 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].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].stage = .VERTEX;
desc.uniform_blocks[0].layout = .STD140; 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].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"; desc.uniform_blocks[0].glsl_uniforms[0].glsl_name = "debugline_vs_params";
case .GLES3; case .GLES3;
desc.vertex_func.source = xx *vs_debugline_source_glsl300es; 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.source = xx *fs_debugline_source_glsl300es;
desc.fragment_func.entry = "main"; desc.fragment_func.entry = "main";
desc.attrs[0].base_type = .FLOAT; 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].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].stage = .VERTEX;
desc.uniform_blocks[0].layout = .STD140; 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].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"; desc.uniform_blocks[0].glsl_uniforms[0].glsl_name = "debugline_vs_params";
case .METAL_MACOS; case .METAL_MACOS;
desc.vertex_func.source = xx *vs_debugline_source_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.fragment_func.entry = "main0";
desc.attrs[0].base_type = .FLOAT; desc.attrs[0].base_type = .FLOAT;
desc.attrs[1].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].stage = .VERTEX;
desc.uniform_blocks[0].layout = .STD140; 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; desc.uniform_blocks[0].msl_buffer_n = 0;
} }
return desc; return desc;

View File

@ -1,16 +1,44 @@
@vs vs_debugline @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 vec4 a_col;
in float a_width; // in pixels
layout(binding=0) uniform debugline_vs_params { layout(binding=0) uniform debugline_vs_params {
mat4 mvp; mat4 mvp;
vec4 viewport; // xy = size in pixels, zw = unused
}; };
out vec4 v_col; out vec4 v_col;
void main() { 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; v_col = a_col;
} }

View File

@ -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 { Exe_Runner :: struct {
suite_idx : int; suite_idx : int;
test_idx : int; test_idx : int;
cmd_idx : int; cmd_idx : int;
wait_until : float64; wait_until : float64;
test_failed : bool; test_failed : bool;
test_started : bool; test_started : bool;
done : bool; done : bool;
passed : int;
failed : int;
test_deadline : float64;
} }
g_exe_runner : Exe_Runner; 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 :: () { run_exe_tests :: () {
r := *g_exe_runner; r := *g_exe_runner;
if r.done then return; if r.done then return;
@ -20,8 +44,7 @@ run_exe_tests :: () {
if r.suite_idx >= g_test_runner_state.count { if r.suite_idx >= g_test_runner_state.count {
print("[exe tests] All suites complete.\n"); print("[exe tests] All suites complete.\n");
r.done = true; finish_exe_tests(r);
sapp_request_quit();
return; return;
} }
@ -40,15 +63,30 @@ run_exe_tests :: () {
if !r.test_started { if !r.test_started {
print("[exe tests] Starting '%' / '%'\n", suite.name, test.name); 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.cmd_idx >= test.cmds.count {
if r.test_failed { if r.test_failed {
r.failed += 1;
print("[exe tests] FAIL '%' / '%'\n", suite.name, test.name); print("[exe tests] FAIL '%' / '%'\n", suite.name, test.name);
} else { } else {
r.passed += 1;
print("[exe tests] PASS '%' / '%'\n", suite.name, test.name); 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.test_idx += 1;
r.cmd_idx = 0; r.cmd_idx = 0;
r.test_failed = false; r.test_failed = false;

12
src/tests/framework.jai Normal file
View File

@ -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";

View File

@ -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 "world_test.jai";
#load "../editor/rdm_disk_test.jai"; #load "../editor/rdm_disk_test.jai";
#load "engine_exe_tests/index.jai"; #load "engine_exe_tests/index.jai";
#load "exe_tests/index.jai";

View File

@ -268,11 +268,106 @@ test_legacy_load_cursor_fix :: () {
end_suite(s); 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_floor_div_mod();
test_coord_roundtrip(); test_coord_roundtrip();
test_chunk_coord_values(); test_chunk_coord_values();
test_world_save_load_roundtrip(); test_world_save_load_roundtrip();
test_world_json_chunk_offsets(); test_world_json_chunk_offsets();
test_legacy_load_cursor_fix(); test_legacy_load_cursor_fix();
test_entity_save_load_roundtrip();
test_entity_unknown_names();
} }

View File

@ -59,18 +59,59 @@ note_to_autoedit_conf :: (notes: []string) -> Autoedit_Conf {
return .{}; 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); autoconf := note_to_autoedit_conf(notes);
builder : String_Builder; 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, "GR.label(r, \"%\", *t_label_left(theme));\n", name);
print_to_builder(*builder, "r.y += r.h;\n"); 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 { if autoconf.kind == .SLIDER {
print_to_builder(*builder, "GR.slider(r, *value.%, %, %, %, *theme.slider_theme);\n", name, autoconf.min, autoconf.max, autoconf.step); print_to_builder(*builder, "GR.slider(r, *value.%, %, %, %, *theme.slider_theme);\n", name, autoconf.min, autoconf.max, autoconf.step);
} else { } else {
print_to_builder(*builder, "GR.number_input(r, tprint(\"\%\", value.%), *value.%, %, %, *number_theme);\n", name, name, autoconf.min, autoconf.max); 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) { } else if type == type_info(Vector3) {
if autoconf.kind == .DEFAULT { if autoconf.kind == .DEFAULT {
print_to_builder(*builder, "{\n"); print_to_builder(*builder, "{\n");
@ -83,7 +124,7 @@ 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, "r.w = orig_w; r.x = orig_x;\n");
print_to_builder(*builder, "}\n"); print_to_builder(*builder, "}\n");
} else if autoconf.kind == .COLOR { } 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);
} }
} }
@ -106,7 +147,10 @@ autoedit :: (rect: GR.Rect, value: *$T, theme: *GR.Overall_Theme, identifier: s3
ti := type_info(T); ti := type_info(T);
#assert #run type_info(T).type == .STRUCT "Autoedit only works for structs"; #assert #run type_info(T).type == .STRUCT "Autoedit only works for structs";
for ti.members { 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); return builder_to_string(*builder);
} }

View File

@ -91,6 +91,8 @@ World :: struct {
chunks : Table(Chunk_Key, Chunk, chunk_key_hash, chunk_key_compare); chunks : Table(Chunk_Key, Chunk, chunk_key_hash, chunk_key_compare);
emitter_instances : [..]Particle_Emitter_Instance; emitter_instances : [..]Particle_Emitter_Instance;
notes : [..]Editor_Note; notes : [..]Editor_Note;
entities : [..]*Entity;
next_entity_id : u32;
rdm_overrides : [..]Rdm_Instance_Override; rdm_overrides : [..]Rdm_Instance_Override;
rdm_lookup : [..]Rdm_Atlas_Entry; // populated by bake (Step 5) and by loader (Step 6) 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); load_world(name);
} @Command; } @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 :: () { init_world_system :: () {
Pool.set_allocators(*current_world.pool); Pool.set_allocators(*current_world.pool);
} }
@ -222,6 +251,10 @@ unload_current_world :: () {
array_free(chunk.groups); array_free(chunk.groups);
} }
deinit(*current_world.world.chunks); 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); array_free(current_world.world.rdm_lookup);
Pool.reset(*current_world.pool); Pool.reset(*current_world.pool);
current_world.valid = false; current_world.valid = false;
@ -353,6 +386,11 @@ resolve_emitter_definitions :: (world: *World) {
for *inst: world.emitter_instances { for *inst: world.emitter_instances {
inst.definition = get_emitter_def(inst.definition_name); 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 :: () { clear_world :: () {
@ -384,6 +422,7 @@ World_Json :: struct {
chunks : [..]World_Json_Chunk; chunks : [..]World_Json_Chunk;
emitters : [..]World_Json_Emitter; emitters : [..]World_Json_Emitter;
notes : [..]World_Json_Note; notes : [..]World_Json_Note;
entities : [..]World_Json_Entity;
rdm_overrides : [..]World_Json_Rdm_Override; rdm_overrides : [..]World_Json_Rdm_Override;
} }
@ -424,6 +463,17 @@ World_Json_Note :: struct {
position : [3]s32; 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 { World_Json_Rdm_Override :: struct {
x : s32; x : s32;
y : s32; y : s32;
@ -619,7 +669,7 @@ save_world :: (world: *World) -> (json: string, chunks_bin: string) {
} }
wj: World_Json; wj: World_Json;
wj.version = 4; wj.version = 5;
wj.name = world.name; wj.name = world.name;
wj.config = world_config_to_json(*world.conf); 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); 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 { for ov: world.rdm_overrides {
size := ifx ov.rdm_size > 0 then ov.rdm_size else RDM_DEFAULT_SIZE; 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}); 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); 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 { for jov: wj.rdm_overrides {
size := ifx jov.rdm_size > 0 then jov.rdm_size else RDM_DEFAULT_SIZE; 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}); array_add(*world.rdm_overrides, .{x=jov.x, y=jov.y, z=jov.z, rdm_enabled=jov.rdm_enabled, rdm_size=size});

View File

@ -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 :: () { game_engine_config :: () {
} }

View File

@ -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": [
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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": [
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -1,5 +1,5 @@
{ {
"version": 4, "version": 5,
"name": "test_world", "name": "test_world",
"config": { "config": {
"skyBase": [ "skyBase": [
@ -23,8 +23,8 @@
"sunPosition": [ "sunPosition": [
0.371391,0.557086,0.742781 0.371391,0.557086,0.742781
], ],
"sunIntensity": 2, "sunIntensity": 1,
"skyIntensity": 1, "skyIntensity": 0.3,
"hasClouds": 1, "hasClouds": 1,
"planeHeight": 0, "planeHeight": 0,
"animatePlaneHeight": 1, "animatePlaneHeight": 1,
@ -33,12 +33,17 @@
], ],
"deepColor": [ "deepColor": [
1,1,1 1,1,1
] ],
"waterShininess": 64
}, },
"chunks": [ "chunks": [
], ],
"emitters": [ "emitters": [
], ],
"notes": [ "notes": [
],
"entities": [
],
"rdm_overrides": [
] ]
} }

Binary file not shown.