Compare commits

...

9 Commits

Author SHA1 Message Date
veclavtalica
66c525a0d4 /apps/demos/crawl: visuals and stuff 2025-02-21 01:13:53 +03:00
veclavtalica
70fab28158 /apps/twnlua: catch self-include stack busting 2025-02-21 00:18:22 +03:00
veclavtalica
80a4ae3d0e progress on /apps/demos/crawl 2025-02-20 22:20:02 +03:00
veclavtalica
bd3b090f6f add /apps/demos/crawl 2025-02-20 20:10:37 +03:00
veclavtalica
6eb0730c52 mark identity parameter in log_ functions optional 2025-02-20 19:52:14 +03:00
veclavtalica
a231d650f2 twn_util.c: add file_read() 2025-02-20 19:51:52 +03:00
veclavtalica
e15975bfaa update to lua template 2025-02-20 17:25:11 +03:00
veclavtalica
b67bc92857 remove optional by pointer texture_region parameters 2025-02-20 16:19:03 +03:00
veclavtalica
991196f7c8 update to docs 2025-02-20 16:03:50 +03:00
21 changed files with 355 additions and 88 deletions

BIN
apps/demos/crawl/data/assets/brick.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/demos/crawl/data/assets/pebbles.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,32 @@
@map
-- Grid of symbols defined by @classes section.
#####
#.@.#
#...#
##.######
#...X...#
#./.#####
#####
@classes
-- Defines classes under symbols, which could have properties attached.
# stone_wall
wall_texture : /assets/brick.png
. stone_floor
tile_texture : /assets/pebbles.png
@ player_spawn
unique :
face : south
hold : torch
tile_texture : /assets/pebbles.png
X door
open_on_signal : sg_torch0
tile_texture : /assets/pebbles.png
/ lever
on_interact_emit : sg_torch0
tile_texture : /assets/pebbles.png
@meta
-- Arbitrary sections could be defined with value pairs.
description : Test Level! Just two square rooms and a tunnel opened by lever.

View File

@ -0,0 +1,64 @@
require("string")
require("level")
require("render")
function lerp(a, b, x)
return a + ((b - a) * x)
end
function qlerp(a, b, x)
return lerp(a, b, x * x)
end
function game_tick()
if ctx.udata == nil then
ctx.udata = {
level = load_level("levels/00.lvl")
}
ctx.udata.player = {
position = ctx.udata.level.classes.player_spawn.position,
position_lerp = ctx.udata.level.classes.player_spawn.position,
direction = { x = 1, y = 0, z = 0 },
direction_lerp = { x = 1, y = 0, z = 0 },
}
end
input_action { control = "A", name = "turn_left" }
input_action { control = "D", name = "turn_right" }
input_action { control = "W", name = "walk_forward" }
input_action { control = "S", name = "walk_backward" }
if input_action_just_released { name = "turn_left" } then
ctx.udata.player.direction = { x = ctx.udata.player.direction.z, y = ctx.udata.player.direction.y, z = -ctx.udata.player.direction.x }
end
if input_action_just_released { name = "turn_right" } then
ctx.udata.player.direction = { x = -ctx.udata.player.direction.z, y = ctx.udata.player.direction.y, z = ctx.udata.player.direction.x }
end
local direction = { x = 0, z = 0 }
if input_action_just_released { name = "walk_forward" } then
direction = { x = direction.x + ctx.udata.player.direction.z, z = direction.z + ctx.udata.player.direction.x }
end
if input_action_just_released { name = "walk_backward" } then
direction = { x = direction.x - ctx.udata.player.direction.z, z = direction.z - ctx.udata.player.direction.x }
end
ctx.udata.player.position = { x = ctx.udata.player.position.x + direction.x, y = ctx.udata.player.position.y + direction.z }
ctx.udata.player.position_lerp.x = qlerp(ctx.udata.player.position_lerp.x, ctx.udata.player.position.x, ctx.frame_duration * 30)
ctx.udata.player.position_lerp.y = qlerp(ctx.udata.player.position_lerp.y, ctx.udata.player.position.y, ctx.frame_duration * 30)
ctx.udata.player.direction_lerp.x = qlerp(ctx.udata.player.direction_lerp.x, ctx.udata.player.direction.x, ctx.frame_duration * 40)
ctx.udata.player.direction_lerp.z = qlerp(ctx.udata.player.direction_lerp.z, ctx.udata.player.direction.z, ctx.frame_duration * 40)
draw_camera {
position = {
x = ctx.udata.player.position_lerp.y * 2 + 1,
y = 1,
z = ctx.udata.player.position_lerp.x * 2 + 1,
},
direction = ctx.udata.player.direction_lerp,
}
render_dungeon(ctx.udata.level)
end

View File

@ -0,0 +1,87 @@
function load_level(file)
local f = file_read { file = file }
local result = {
classes = {
void = { },
},
glossary = {
[" "] = "void",
},
grid = {},
map = {},
size = { x = 0, y = 0 },
}
-- iterate over lines
local section, subsection = "none", "none"
local from = 1
local start, limit = string.find(f, "\n", from)
while start do
local line = string.sub(f, from, start - 1)
-- skip over
if #line == 0 or line:find("^%-%-%s*") then
goto skip
-- start new section
elseif line:find("^@%g+") then
section = line:sub(2); subsection = "none"
-- decode map one line at a time
elseif section == "map" then
local l = #result.map + 1
if result.size.x < #line then
result.size.x = #line
end
result.map[l] = {}
for i = 1, #line do
result.map[l][i] = line:sub(i,i)
end
-- templates to expand
elseif section == "classes" then
-- properties
if line:find("^ %g+") then
local _, _, property, value = line:find("^ (%g+)%s?:%s?(.*)")
result.classes[subsection][property] = value
goto skip
end
local symbol, classname = line:sub(1,1), line:sub(3)
result.classes[classname] = {
symbol = symbol,
}
result.glossary[symbol] = classname
subsection = classname
elseif section ~= "none" then
local _, _, property, value = line:find("^(%g+)%s?:%s?(.*)")
if result[section] == nil then
result[section] = {}
end
result[section][property] = value
end
::skip::
from = limit + 1
start, limit = string.find(f, "\n", from)
end
-- post process
for y = 1, #result.map do
result.grid[y] = {}
for x = 1, result.size.x do
-- past defined for line
local symbol
if x > #result.map[y] then symbol = " "
else symbol = result.map[y][x]
end
local class = result.classes[result.glossary[symbol]]
if class["unique"] ~= nil then
class.position = { x = x, y = y }
end
result.grid[y][x] = class
::continue::
end
end
result.size.y = #result.map
print(result.meta.description)
return result
end

View File

@ -0,0 +1,59 @@
function render_dungeon(dungeon)
for y = 1, dungeon.size.y do
for x = 1, dungeon.size.x do
if dungeon.grid[y][x].wall_texture ~= nil then
draw_quad {
texture = dungeon.grid[y][x].wall_texture,
v3 = { x = y * 2, y = 2, z = x * 2 },
v2 = { x = y * 2, y = 0, z = x * 2 },
v1 = { x = y * 2 + 2, y = 0, z = x * 2 },
v0 = { x = y * 2 + 2, y = 2, z = x * 2 },
texture_region = { w = 128, h = 128 },
}
draw_quad {
texture = dungeon.grid[y][x].wall_texture,
v3 = { x = y * 2 + 2, y = 2, z = x * 2 },
v2 = { x = y * 2 + 2, y = 0, z = x * 2 },
v1 = { x = y * 2 + 2, y = 0, z = x * 2 + 2 },
v0 = { x = y * 2 + 2, y = 2, z = x * 2 + 2 },
texture_region = { w = 128, h = 128 },
}
draw_quad {
texture = dungeon.grid[y][x].wall_texture,
v3 = { x = y * 2 + 2, y = 2, z = x * 2 + 2 },
v2 = { x = y * 2 + 2, y = 0, z = x * 2 + 2 },
v1 = { x = y * 2, y = 0, z = x * 2 + 2 },
v0 = { x = y * 2, y = 2, z = x * 2 + 2 },
texture_region = { w = 128, h = 128 },
}
draw_quad {
texture = dungeon.grid[y][x].wall_texture,
v3 = { x = y * 2, y = 2, z = x * 2 + 2 },
v2 = { x = y * 2, y = 0, z = x * 2 + 2 },
v1 = { x = y * 2, y = 0, z = x * 2 },
v0 = { x = y * 2, y = 2, z = x * 2 },
texture_region = { w = 128, h = 128 },
}
elseif dungeon.grid[y][x].tile_texture ~= nil then
draw_quad {
texture = dungeon.grid[y][x].tile_texture,
v0 = { x = y * 2 + 2, y = 0, z = x * 2 },
v1 = { x = y * 2, y = 0, z = x * 2 },
v2 = { x = y * 2, y = 0, z = x * 2 + 2 },
v3 = { x = y * 2 + 2, y = 0, z = x * 2 + 2},
texture_region = { w = 128, h = 128 },
}
draw_quad {
texture = dungeon.grid[y][x].tile_texture,
v3 = { x = y * 2 + 2, y = 2, z = x * 2 },
v2 = { x = y * 2, y = 2, z = x * 2 },
v1 = { x = y * 2, y = 2, z = x * 2 + 2 },
v0 = { x = y * 2 + 2, y = 2, z = x * 2 + 2},
texture_region = { w = 128, h = 128 },
}
end
end
end
end

View File

@ -0,0 +1,27 @@
# This file contains everything about the engine and your game that can be
# configured before it runs.
#
# Optional settings are commented out, with their default values shown.
# Invalid values in these settings will be ignored.
# Data about your game as an application
[about]
title = "Template"
developer = "You"
app_id = "template"
dev_id = "you"
# Game runtime details
[game]
resolution = [ 640, 480 ]
interpreter = "$TWNROOT/apps/twnlua"
#debug = true
# Engine tweaks. You probably don't need to change these
[engine]
#ticks_per_second = 60 # minimum of 8
#keybind_slots = 3 # minimum of 1
#texture_atlas_size = 2048 # minimum of 32
#font_texture_size = 2048 # minimum of 1024
#font_oversampling = 4 # minimum of 0
#font_filtering = "linear" # possible values: "nearest", "linear"

View File

@ -179,7 +179,7 @@ static void draw_terrain(SceneIngame *scn) {
draw_billboard("/assets/grasses/10.png",
(Vec3){ (float)x, d0 + 0.15f, (float)y },
(Vec2){0.3f, 0.3f},
NULL,
(Rect){0},
(Color){255, 255, 255, 255}, true);
}
}

View File

@ -1,8 +1,7 @@
-- called every frame, with constant delta time
function game_tick()
-- ctx.initialization_needed is true first frame and every time dynamic reload is performed
if ctx.initialization_needed then
-- ctx.udata persists on reload
-- ctx.udata persists on code reload
if ctx.udata == nil then
ctx.udata = {}
end
end

View File

@ -25,6 +25,12 @@ void bindgen_upload_context(lua_State *L);
static int physfs_loader(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
static const char *name_breaker = NULL;
if (name_breaker && SDL_strcmp(name, name_breaker) == 0) {
log_critical("Recursive load on itself from lua module (%s)", name_breaker);
return 0;
} name_breaker = name;
/* replace dots with path slashes */
char *path_copy = SDL_strdup(name);
char *ch = NULL;

View File

@ -54,14 +54,15 @@
<li><b>G</b> for gamedev; guides and FAQs on game making.
</ul>
</blockquote>
<p><a name="abi"></a><strong>T1.3 </strong><strong>ABI</strong>
<p><a name="abi"></a><strong>T1.3 </strong><strong>Procedure Interface</strong>
<blockquote>
<p>For native code ABI defines convention to ease tooling integration.
<p>For native code ABI defines convention to ease tooling integration. Platform/compiler C ABI is assumed,
but it should be trivially expressible in JSON-RPC as well.
<ul>
<li>32 bit floating point is the only numeric type.
<li>Procedure parameters can only use basic types, with no aggregates. Exceptions are Vec, Rect and Color types.
(see /include/twn_types.h)
<li>Enum types are not allowed, as they decay to integer type, identity strings are used instead.
<li>Enum types are allowed, but must be converted to floats. Identity strings are preferred.
<li>No opaque nor pointer types allowed, use string keys if needed. Think of it as data base relations.
<li>Only null terminated string is allowed as a sequential type in both parameters and returns.
<li>Return value could be a simple aggregate that is translatable to a dictionary of primitives, without nesting.
@ -70,6 +71,8 @@
<li>Parameter names should not collide with keywords of any language that is targeted; if so happens, parameter alias could be added.
Currently forbidden: <b>repeat</b>.
<li>Procedure can't have more than 8 parameters.
<li>Decimal portions of floating points are lossy both due to rounding errors and text representation,
thus they cannot be relied to hold for equality. Integer parts of floats are good up to 2^24.
</ul>
</blockquote>
</body>

View File

@ -20,6 +20,7 @@
<blockquote style="margin-top:0">
<p style="margin:0">T1.1 <a href="about-townengine.html#introduction">Introduction</a></p>
<p style="margin:0">T1.2 <a href="about-townengine.html#wiki">Wiki</a></p>
<p style="margin:0">T1.3 <a href="about-townengine.html#abi">Procedure Interface</a></p>
</blockquote>
<p style="margin-bottom:0"><a name="input-system"></a>T2. </strong><a href="input-system.html">Input System</strong></a></p>
<blockquote style="margin-top:0">

View File

@ -10,7 +10,7 @@
/* TODO: combine flip_x and flip_y into a flip_mask with enum */
TWN_API void draw_sprite(char const *texture,
Rect rect,
Rect const *texture_region, /* optional, default: NULL */
Rect texture_region, /* optional, default: all 0 */
Color color, /* optional, default: all 255 */
float rotation, /* optional, default: 0 */
bool flip_x, /* optional, default: false */
@ -76,8 +76,8 @@ TWN_API void draw_quad(char const *texture,
TWN_API void draw_billboard(char const *texture,
Vec3 position,
Vec2 size,
Rect const *texture_region, /* optional, default: NULL */
Color color, /* optional, default: all 255 */
Rect texture_region, /* optional, default: NULL */
Color color, /* optional, default: all 0 */
bool cylindrical); /* optional, default: false */
TWN_API void draw_camera_2d(Vec2 position, /* optional, default: (0, 0) */

View File

@ -42,6 +42,9 @@
#endif /* TWN_NOT_C */
/* read file to null terminated string, it is freed when the frame ends */
TWN_API char const *file_read(char const *file);
/* calculates the overlap of two rectangles */
TWN_API Rect rect_overlap(Rect a, Rect b);
/* returns true if two rectangles are intersecting */

View File

@ -59,7 +59,7 @@
"params": [
{ "name": "texture", "type": "char *" },
{ "name": "rect", "type": "Rect" },
{ "name": "texture_region", "type": "Rect *", "default": {} },
{ "name": "texture_region", "type": "Rect", "default": { "x": 0, "y": 0, "w": 0, "h": 0 } },
{ "name": "color", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } },
{ "name": "rotation", "type": "float", "default": 0.0 },
{ "name": "flip_x", "type": "bool", "default": false },
@ -180,7 +180,7 @@
{ "name": "texture", "type": "char *" },
{ "name": "position", "type": "Vec3" },
{ "name": "size", "type": "Vec2" },
{ "name": "texture_region", "type": "Rect *", "default": {} },
{ "name": "texture_region", "type": "Rect", "default": { "x": 0, "y": 0, "w": 0, "h": 0 } },
{ "name": "color", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } },
{ "name": "cylindrical", "type": "bool", "default": false }
]
@ -264,6 +264,16 @@
]
},
"file_read": {
"module": "util",
"symbol": "read",
"header": "twn_util.h",
"params": [
{ "name": "file", "type": "char *" }
],
"return": "char *"
},
"timer_tick_seconds": {
"module": "util",
"symbol": "tick_seconds",
@ -298,7 +308,7 @@
"header": "twn_util.h",
"params": [
{ "name": "value", "type": "Vec2" },
{ "name": "identity", "type": "char *" }
{ "name": "identity", "type": "char *", "default": {} }
]
},
@ -308,7 +318,7 @@
"header": "twn_util.h",
"params": [
{ "name": "value", "type": "Vec3" },
{ "name": "identity", "type": "char *" }
{ "name": "identity", "type": "char *", "default": {} }
]
},
@ -318,7 +328,7 @@
"header": "twn_util.h",
"params": [
{ "name": "value", "type": "Rect" },
{ "name": "identity", "type": "char *" }
{ "name": "identity", "type": "char *", "default": {} }
]
},

View File

@ -12,7 +12,7 @@
void draw_billboard(char const *texture,
Vec3 position,
Vec2 size,
Rect const *texture_region,
Rect texture_region,
Color color,
bool cylindrical)
{
@ -26,16 +26,19 @@ void draw_billboard(char const *texture,
batch_p = &ctx.billboard_batches[hmlenu(ctx.billboard_batches) - 1]; /* TODO: can last index be used? */
}
bool const texture_region_valid = fabsf(texture_region.w - texture_region.h) > 0.00001f
&& fabsf(0.0f - texture_region.w) > 0.00001f;
struct SpaceBillboard billboard = {
.color = color,
.cylindrical = cylindrical,
.position = position,
.size = size,
.texture_region_opt_set = texture_region != NULL,
.texture_region_opt_set = texture_region_valid,
};
if (texture_region)
billboard.texture_region_opt = *texture_region;
if (texture_region_valid)
billboard.texture_region_opt = texture_region;
struct SpaceBillboard *billboards = (struct SpaceBillboard *)(void *)batch_p->value.primitives;

View File

@ -19,13 +19,18 @@
*/
void draw_sprite(char const *path,
Rect rect,
Rect const *texture_region, /* optional, default: NULL */
Rect texture_region, /* optional, default: 0 */
Color color, /* optional, default: all 255 */
float rotation, /* optional, default: 0 */
bool flip_x, /* optional, default: false */
bool flip_y, /* optional, default: false */
bool stretch)
{
/* if .w and .h are zeroed then assume whole region */
/* TODO: don't check here, just move to redner code ? */
bool const texture_region_valid = fabsf(texture_region.w - texture_region.h) > 0.00001f
&& fabsf(0.0f - texture_region.w) > 0.00001f;
SpritePrimitive sprite = {
.rect = rect,
.color = color,
@ -34,11 +39,11 @@ void draw_sprite(char const *path,
.flip_x = flip_x,
.flip_y = flip_y,
.repeat = !stretch,
.texture_region_opt_set = texture_region != NULL,
.texture_region_opt_set = texture_region_valid,
};
if (texture_region)
sprite.texture_region_opt = *texture_region;
if (texture_region_valid)
sprite.texture_region_opt = texture_region;
Primitive2D primitive = {
.type = PRIMITIVE_2D_SPRITE,
@ -54,7 +59,7 @@ void draw_sprite_args(const DrawSpriteArgs args) {
bool const flip_x = m_or(args, flip_x, false);
bool const flip_y = m_or(args, flip_y, false);
bool const stretch = m_or(args, stretch, false);
Rect const *texture_region = m_is_set(args, texture_region) ? &args.texture_region_opt : NULL;
Rect const texture_region = m_or(args, texture_region, ((Rect){0}));
draw_sprite(args.texture, args.rect, texture_region, color, rotation, flip_x, flip_y, stretch);
}

View File

@ -754,6 +754,11 @@ static bool try_mounting_root_pack(char *path) {
}
static void garbage_collect(void) {
file_read_garbage_collect();
}
int enter_loop(int argc, char **argv) {
profile_start("startup");
@ -866,6 +871,7 @@ int enter_loop(int argc, char **argv) {
/* dispatch all filewatch driven events, such as game object and asset pack reload */
filewatch_poll();
main_loop();
garbage_collect();
}
if (ctx.game.debug)

View File

@ -154,6 +154,24 @@ char *file_to_str(const char *path) {
return str_out;
}
static char **read_files;
char const *file_read(char const *file) {
char *s = file_to_str(file);
if (s) arrpush(read_files, s);
return s;
}
void file_read_garbage_collect(void) {
for (size_t i = 0; i < arrlenu(read_files); ++i)
SDL_free(read_files[i]);
arrfree(read_files);
read_files = NULL;
}
bool file_exists(const char *path) {
return PHYSFS_exists(path);

View File

@ -29,6 +29,8 @@ char *expand_asterisk(const char *mask, const char *to);
void profile_list_stats(void);
void file_read_garbage_collect(void);
/* http://www.azillionmonkeys.com/qed/sqroot.html */
static inline float fast_sqrt(float x)
{

View File

@ -297,58 +297,6 @@ static void* array_realloc(void* ptr, fastObjUInt n, fastObjUInt b)
}
static
void* file_open(const char* path, void* user_data)
{
(void)(user_data);
return fopen(path, "rb");
}
static
void file_close(void* file, void* user_data)
{
FILE* f;
(void)(user_data);
f = (FILE*)(file);
fclose(f);
}
static
size_t file_read(void* file, void* dst, size_t bytes, void* user_data)
{
FILE* f;
(void)(user_data);
f = (FILE*)(file);
return fread(dst, 1, bytes, f);
}
static
unsigned long file_size(void* file, void* user_data)
{
FILE* f;
long p;
long n;
(void)(user_data);
f = (FILE*)(file);
p = ftell(f);
fseek(f, 0, SEEK_END);
n = ftell(f);
fseek(f, p, SEEK_SET);
if (n > 0)
return (unsigned long)(n);
else
return 0;
}
static
char* string_copy(const char* s, const char* e)
{
@ -1408,18 +1356,6 @@ void fast_obj_destroy(fastObjMesh* m)
}
fastObjMesh* fast_obj_read(const char* path)
{
fastObjCallbacks callbacks;
callbacks.file_open = file_open;
callbacks.file_close = file_close;
callbacks.file_read = file_read;
callbacks.file_size = file_size;
return fast_obj_read_with_callbacks(path, &callbacks, 0);
}
fastObjMesh* fast_obj_read_with_callbacks(const char* path, const fastObjCallbacks* callbacks, void* user_data)
{
fastObjData data;