From adbd2511beec8f1caa1752bdfd755cc2f62ba425 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 9 Mar 2024 08:43:26 -0800 Subject: Make isogfx a library instead of an executable. --- game/src/game.c | 27 ++- gfx-app/include/gfx/gfx_app.h | 53 ++--- gfx-app/src/gfx_app.c | 6 +- gfx-iso/CMakeLists.txt | 29 ++- gfx-iso/app/app.h | 12 -- gfx-iso/app/checkerboard.c | 120 ----------- gfx-iso/app/checkerboard.h | 9 - gfx-iso/app/isogfx-demo.c | 79 -------- gfx-iso/app/isogfx-demo.h | 9 - gfx-iso/app/main.c | 199 ------------------ gfx-iso/asset/mkasset.py | 324 ------------------------------ gfx-iso/demos/CMakeLists.txt | 2 + gfx-iso/demos/checkerboard/CMakeLists.txt | 15 ++ gfx-iso/demos/checkerboard/checkerboard.c | 114 +++++++++++ gfx-iso/demos/isomap/CMakeLists.txt | 15 ++ gfx-iso/demos/isomap/isomap.c | 72 +++++++ gfx-iso/include/isogfx/app.h | 22 ++ gfx-iso/src/app.c | 198 ++++++++++++++++++ gfx-iso/tools/mkasset.py | 324 ++++++++++++++++++++++++++++++ 19 files changed, 827 insertions(+), 802 deletions(-) delete mode 100644 gfx-iso/app/app.h delete mode 100644 gfx-iso/app/checkerboard.c delete mode 100644 gfx-iso/app/checkerboard.h delete mode 100644 gfx-iso/app/isogfx-demo.c delete mode 100644 gfx-iso/app/isogfx-demo.h delete mode 100644 gfx-iso/app/main.c delete mode 100644 gfx-iso/asset/mkasset.py create mode 100644 gfx-iso/demos/CMakeLists.txt create mode 100644 gfx-iso/demos/checkerboard/CMakeLists.txt create mode 100644 gfx-iso/demos/checkerboard/checkerboard.c create mode 100644 gfx-iso/demos/isomap/CMakeLists.txt create mode 100644 gfx-iso/demos/isomap/isomap.c create mode 100644 gfx-iso/include/isogfx/app.h create mode 100644 gfx-iso/src/app.c create mode 100644 gfx-iso/tools/mkasset.py diff --git a/game/src/game.c b/game/src/game.c index c720656..dc4ab84 100644 --- a/game/src/game.c +++ b/game/src/game.c @@ -39,6 +39,10 @@ static const int WIDTH = 1350; static const int HEIGHT = 900; static const int MAX_FPS = 60; +typedef struct GfxAppState { + Game game; +} GfxAppState; + /// Initialize the game's plugin. static bool init_plugin(Game* game) { assert(game); @@ -113,17 +117,11 @@ static void resize_plugin(Game* game, int width, int height) { void app_end(Game* game); -bool app_init(const GfxAppDesc* desc, void** app_state) { - assert(desc); - - if (desc->argc <= 1) { - LOGE("Usage: %s [plugin args]", desc->argv[0]); - return false; - } +bool app_init(Game* game, int argc, const char** argv) { + assert(game); - Game* game = calloc(1, sizeof(Game)); - if (!game) { - LOGE("Failed to allocate game state"); + if (argc <= 1) { + LOGE("Usage: %s [plugin args]", argv[0]); return false; } @@ -131,8 +129,8 @@ bool app_init(const GfxAppDesc* desc, void** app_state) { // // Here we consume the arg so that plugins receive the remainder // args starting from 0. - game->argc = desc->argc - 1; - game->argv = desc->argv + 1; + game->argc = argc - 1; + game->argv = argv + 1; char exe_path_buf[NAME_MAX] = {0}; if (readlink("/proc/self/exe", exe_path_buf, sizeof(exe_path_buf)) == -1) { @@ -152,7 +150,7 @@ bool app_init(const GfxAppDesc* desc, void** app_state) { goto cleanup; } - const char* plugin = desc->argv[1]; + const char* plugin = argv[1]; if (!(game->plugin = load_plugin(game->plugin_engine, plugin))) { goto cleanup; } @@ -168,7 +166,6 @@ bool app_init(const GfxAppDesc* desc, void** app_state) { goto cleanup; } - *app_state = game; return true; cleanup: @@ -223,4 +220,4 @@ void app_resize(Game* game, int width, int height) { resize_plugin(game, width, height); } -GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS); +GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS, "Game"); diff --git a/gfx-app/include/gfx/gfx_app.h b/gfx-app/include/gfx/gfx_app.h index 86c502a..ffff4bc 100644 --- a/gfx-app/include/gfx/gfx_app.h +++ b/gfx-app/include/gfx/gfx_app.h @@ -2,6 +2,8 @@ #include +typedef struct GfxAppState GfxAppState; + /// Application settings. typedef struct GfxAppDesc { int argc; // Number of application arguments. @@ -11,21 +13,22 @@ typedef struct GfxAppDesc { int max_fps; // Desired maximum display framerate. 0 to disable. double update_delta_time; // Desired delta time between frame updates. const char* title; // Window title. + GfxAppState* app_state; } GfxAppDesc; -typedef bool (*GfxAppInit)(const GfxAppDesc*, void** app_state); -typedef void (*GfxAppUpdate)(void* app_state, double t, double dt); -typedef void (*GfxAppRender)(void* app_state); -typedef void (*GfxAppResize)(void* app_state, int width, int height); -typedef void (*GfxAppShutdown)(void* app_state); +typedef bool (*GfxAppInit)(GfxAppState*, int argc, const char** argv); +typedef void (*GfxAppShutdown)(GfxAppState*); +typedef void (*GfxAppUpdate)(GfxAppState*, double t, double dt); +typedef void (*GfxAppRender)(GfxAppState*); +typedef void (*GfxAppResize)(GfxAppState*, int width, int height); /// Application callback functions. typedef struct GfxAppCallbacks { GfxAppInit init; + GfxAppShutdown shutdown; GfxAppUpdate update; GfxAppRender render; GfxAppResize resize; - GfxAppShutdown shutdown; } GfxAppCallbacks; typedef enum Key { @@ -68,21 +71,25 @@ bool gfx_is_key_pressed(Key); /// Define a main function that initializes and puts the application in a loop. /// See also: gfx_app_run(). -#define GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS) \ - int main(int argc, const char** argv) { \ - gfx_app_run( \ - &(GfxAppDesc){ \ - .argc = argc, \ - .argv = argv, \ - .width = WIDTH, \ - .height = HEIGHT, \ - .max_fps = MAX_FPS, \ - .update_delta_time = MAX_FPS > 0 ? 1.0 / (double)MAX_FPS : 0.0}, \ - &(GfxAppCallbacks){ \ - .init = (GfxAppInit)app_init, \ - .update = (GfxAppUpdate)app_update, \ - .render = (GfxAppRender)app_render, \ - .resize = (GfxAppResize)app_resize, \ - .shutdown = (GfxAppShutdown)app_end}); \ - return 0; \ +#define GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS, TITLE) \ + int main(int argc, const char** argv) { \ + GfxAppState app_state = {0}; \ + gfx_app_run( \ + &(GfxAppDesc){ \ + .argc = argc, \ + .argv = argv, \ + .width = WIDTH, \ + .height = HEIGHT, \ + .max_fps = MAX_FPS, \ + .update_delta_time = MAX_FPS > 0 ? 1.0 / (double)MAX_FPS : 0.0, \ + .title = TITLE, \ + .app_state = &app_state, \ + }, \ + &(GfxAppCallbacks){ \ + .init = (GfxAppInit)app_init, \ + .update = (GfxAppUpdate)app_update, \ + .render = (GfxAppRender)app_render, \ + .resize = (GfxAppResize)app_resize, \ + .shutdown = (GfxAppShutdown)app_end}); \ + return 0; \ } diff --git a/gfx-app/src/gfx_app.c b/gfx-app/src/gfx_app.c index e0211b1..a93756c 100644 --- a/gfx-app/src/gfx_app.c +++ b/gfx-app/src/gfx_app.c @@ -9,7 +9,7 @@ /// Application state. typedef struct GfxApp { - void* app_state; + GfxAppState* app_state; GfxAppCallbacks callbacks; int max_fps; double update_delta_time; @@ -79,6 +79,7 @@ bool gfx_app_run(const GfxAppDesc* desc, const GfxAppCallbacks* callbacks) { bool success = false; + g_gfx_app.app_state = desc->app_state; g_gfx_app.callbacks = *callbacks; g_gfx_app.max_fps = desc->max_fps; g_gfx_app.update_delta_time = desc->update_delta_time; @@ -110,7 +111,8 @@ bool gfx_app_run(const GfxAppDesc* desc, const GfxAppCallbacks* callbacks) { glfwMakeContextCurrent(g_gfx_app.window); // Initialize the application's state before setting any callbacks. - if (!(*g_gfx_app.callbacks.init)(desc, &g_gfx_app.app_state)) { + if (!(*g_gfx_app.callbacks.init)( + g_gfx_app.app_state, desc->argc, desc->argv)) { LOGE("Failed to initialize application"); goto cleanup; } diff --git a/gfx-iso/CMakeLists.txt b/gfx-iso/CMakeLists.txt index 993bbb3..673cb68 100644 --- a/gfx-iso/CMakeLists.txt +++ b/gfx-iso/CMakeLists.txt @@ -2,33 +2,42 @@ cmake_minimum_required(VERSION 3.0) project(isogfx) -set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD 17) set(CMAKE_C_STANDARD_REQUIRED On) set(CMAKE_C_EXTENSIONS Off) +# isogfx + add_library(isogfx src/isogfx.c) target_include_directories(isogfx PUBLIC include) -target_link_libraries(isogfx PRIVATE +target_link_libraries(isogfx PUBLIC filesystem mem mempool) target_compile_options(isogfx PRIVATE -Wall -Wextra -Wpedantic) -# Demo +# App -project(isogfx-app) +add_library(isogfx-app + src/app.c) -add_executable(isogfx-app - app/checkerboard.c - app/isogfx-demo.c - app/main.c) +target_include_directories(isogfx-app PUBLIC + include) -target_link_libraries(isogfx-app PRIVATE - gfx +target_link_libraries(isogfx-app PUBLIC gfx-app isogfx) + +target_link_libraries(isogfx-app PRIVATE + gfx) + +target_compile_options(isogfx-app PRIVATE -Wall -Wextra -Wpedantic) + +# Demos + +add_subdirectory(demos) diff --git a/gfx-iso/app/app.h b/gfx-iso/app/app.h deleted file mode 100644 index 25e55eb..0000000 --- a/gfx-iso/app/app.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -typedef struct IsoGfx IsoGfx; -typedef struct IsoGfxApp IsoGfxApp; - -typedef struct IsoGfxApp { - int pixel_scale; // 0 or 1 for 1:1 scale. - void* state; - void (*shutdown)(IsoGfx*, void* state); - void (*update)(IsoGfx*, void* state, double t, double dt); - void (*render)(IsoGfx*, void* state); -} IsoGfxApp; diff --git a/gfx-iso/app/checkerboard.c b/gfx-iso/app/checkerboard.c deleted file mode 100644 index 8b394c4..0000000 --- a/gfx-iso/app/checkerboard.c +++ /dev/null @@ -1,120 +0,0 @@ -#include "isogfx-demo.h" - -#include -#include - -#include -#include -#include -#include - -static const int TILE_WIDTH = 64; -static const int TILE_HEIGHT = TILE_WIDTH / 2; -static const int WORLD_WIDTH = 20; -static const int WORLD_HEIGHT = 20; - -static const TileDesc tile_set[] = { - {.type = TileFromColour, - .width = TILE_WIDTH, - .height = TILE_HEIGHT, - .colour = (Pixel){.r = 0x38, .g = 0x3b, .b = 0x46, .a = 0xff}}, - {.type = TileFromColour, - .width = TILE_WIDTH, - .height = TILE_HEIGHT, - .colour = (Pixel){.r = 0xA5, .g = 0xb3, .b = 0xc0, .a = 0xff}}, - {.type = TileFromColour, - .width = TILE_WIDTH, - .height = TILE_HEIGHT, - .colour = (Pixel){.r = 0xdc, .g = 0x76, .b = 0x84, .a = 0xff}}, -}; - -typedef enum Colour { - Black, - White, - Red, -} Colour; - -typedef struct State { - Tile red; - int xpick; - int ypick; -} State; - -static void make_checkerboard(IsoGfx* iso, Tile black, Tile white) { - assert(iso); - for (int y = 0; y < isogfx_world_height(iso); ++y) { - for (int x = 0; x < isogfx_world_width(iso); ++x) { - const int odd_col = x & 1; - const int odd_row = y & 1; - const Tile value = (odd_row ^ odd_col) == 0 ? black : white; - isogfx_set_tile(iso, x, y, value); - } - } -} - -static void shutdown(IsoGfx* iso, void* app_state) { - assert(iso); - if (app_state) { - free(app_state); - } -} - -static void update(IsoGfx* iso, void* app_state, double t, double dt) { - assert(iso); - assert(app_state); - State* state = (State*)(app_state); - - double mouse_x, mouse_y; - gfx_app_get_mouse_position(&mouse_x, &mouse_y); - - isogfx_pick_tile(iso, mouse_x, mouse_y, &state->xpick, &state->ypick); - - printf("Picked tile: (%d, %d)\n", state->xpick, state->ypick); -} - -static void render(IsoGfx* iso, void* app_state) { - assert(iso); - assert(app_state); - State* state = (State*)(app_state); - - isogfx_render(iso); - if ((state->xpick != -1) && (state->ypick != -1)) { - isogfx_draw_tile(iso, state->xpick, state->ypick, state->red); - } -} - -bool make_checkerboard_app(IsoGfx* iso, IsoGfxApp* app) { - assert(iso); - assert(app); - - State* state = calloc(1, sizeof(State)); - if (!state) { - return false; - } - - if (!isogfx_make_world( - iso, &(WorldDesc){ - .tile_width = TILE_WIDTH, - .tile_height = TILE_HEIGHT, - .world_width = WORLD_WIDTH, - .world_height = WORLD_HEIGHT})) { - goto cleanup; - } - - const Tile black = isogfx_make_tile(iso, &tile_set[Black]); - const Tile white = isogfx_make_tile(iso, &tile_set[White]); - state->red = isogfx_make_tile(iso, &tile_set[Red]); - make_checkerboard(iso, black, white); - isogfx_render(iso); - - app->state = state; - app->shutdown = shutdown; - app->update = update; - app->render = render; - - return true; - -cleanup: - free(state); - return false; -} diff --git a/gfx-iso/app/checkerboard.h b/gfx-iso/app/checkerboard.h deleted file mode 100644 index 61725a5..0000000 --- a/gfx-iso/app/checkerboard.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "app.h" - -#include - -typedef struct IsoGfxApp IsoGfxApp; - -bool make_checkerboard_app(IsoGfx*, IsoGfxApp*); diff --git a/gfx-iso/app/isogfx-demo.c b/gfx-iso/app/isogfx-demo.c deleted file mode 100644 index 9889275..0000000 --- a/gfx-iso/app/isogfx-demo.c +++ /dev/null @@ -1,79 +0,0 @@ -#include "isogfx-demo.h" - -#include -#include - -#include -#include -#include -#include - -typedef struct State { - int xpick; - int ypick; - SpriteSheet stag_sheet; - Sprite stag; -} State; - -static void shutdown(IsoGfx* iso, void* app_state) { - assert(iso); - if (app_state) { - free(app_state); - } -} - -static void update(IsoGfx* iso, void* app_state, double t, double dt) { - assert(iso); - assert(app_state); - State* state = (State*)(app_state); - - double mouse_x, mouse_y; - gfx_app_get_mouse_position(&mouse_x, &mouse_y); - - isogfx_pick_tile(iso, mouse_x, mouse_y, &state->xpick, &state->ypick); - - // printf("Picked tile: (%d, %d)\n", state->xpick, state->ypick); -} - -static void render(IsoGfx* iso, void* app_state) { - assert(iso); - assert(app_state); - State* state = (State*)(app_state); - - isogfx_render(iso); -} - -bool make_demo_app(IsoGfx* iso, IsoGfxApp* app) { - assert(iso); - assert(app); - - State* state = calloc(1, sizeof(State)); - if (!state) { - return false; - } - - if (!isogfx_load_world(iso, "/home/jeanne/assets/tilemaps/demo1.tm")) { - goto cleanup; - } - - if (!isogfx_load_sprite_sheet( - iso, "/home/jeanne/assets/tilesets/scrabling/critters/stag/stag.ss", - &state->stag_sheet)) { - goto cleanup; - } - - state->stag = isogfx_make_sprite(iso, state->stag_sheet); - isogfx_set_sprite_position(iso, state->stag, 5, 4); - - app->pixel_scale = 2; - app->state = state; - app->shutdown = shutdown; - app->update = update; - app->render = render; - - return true; - -cleanup: - free(state); - return false; -} diff --git a/gfx-iso/app/isogfx-demo.h b/gfx-iso/app/isogfx-demo.h deleted file mode 100644 index d099824..0000000 --- a/gfx-iso/app/isogfx-demo.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "app.h" - -#include - -typedef struct IsoGfxApp IsoGfxApp; - -bool make_demo_app(IsoGfx*, IsoGfxApp*); diff --git a/gfx-iso/app/main.c b/gfx-iso/app/main.c deleted file mode 100644 index 050a42f..0000000 --- a/gfx-iso/app/main.c +++ /dev/null @@ -1,199 +0,0 @@ -#include "app.h" -#include "checkerboard.h" -#include "isogfx-demo.h" - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -static const int SCREEN_WIDTH = 1408; -static const int SCREEN_HEIGHT = 960; - -typedef struct State { - Gfx* gfx; - IsoGfx* iso; - IsoGfxApp app; - Texture* screen_texture; - Scene* scene; -} State; - -static bool init(const GfxAppDesc* desc, void** app_state) { - State* state = calloc(1, sizeof(State)); - if (!state) { - return false; - } - - if (!(state->iso = isogfx_new(&(IsoGfxDesc){ - .screen_width = SCREEN_WIDTH, .screen_height = SCREEN_HEIGHT}))) { - goto cleanup; - } - // if (!make_checkerboard_app(state->iso, &state->app)) { - // goto cleanup; - // } - if (!make_demo_app(state->iso, &state->app)) { - goto cleanup; - } - - // Apply pixel scaling if requested by the app. - int texture_width, texture_height; - if (state->app.pixel_scale > 1) { - texture_width = SCREEN_WIDTH / state->app.pixel_scale; - texture_height = SCREEN_HEIGHT / state->app.pixel_scale; - isogfx_resize(state->iso, texture_width, texture_height); - } else { - texture_width = SCREEN_WIDTH; - texture_height = SCREEN_HEIGHT; - } - - if (!(state->gfx = gfx_init())) { - goto cleanup; - } - RenderBackend* render_backend = gfx_get_render_backend(state->gfx); - - if (!(state->screen_texture = gfx_make_texture( - render_backend, &(TextureDesc){ - .width = texture_width, - .height = texture_height, - .dimension = Texture2D, - .format = TextureSRGBA8, - .filtering = NearestFiltering, - .wrap = ClampToEdge, - .mipmaps = false}))) { - goto cleanup; - } - - ShaderProgram* shader = gfx_make_view_texture_shader(render_backend); - if (!shader) { - goto cleanup; - } - - Geometry* geometry = gfx_make_quad_11(render_backend); - if (!geometry) { - goto cleanup; - } - - MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1}; - material_desc.uniforms[0] = (ShaderUniform){ - .type = UniformTexture, - .value.texture = state->screen_texture, - .name = sstring_make("Texture")}; - Material* material = gfx_make_material(&material_desc); - if (!material) { - return false; - } - - const MeshDesc mesh_desc = - (MeshDesc){.geometry = geometry, .material = material, .shader = shader}; - Mesh* mesh = gfx_make_mesh(&mesh_desc); - if (!mesh) { - goto cleanup; - } - - SceneObject* object = - gfx_make_object(&(ObjectDesc){.num_meshes = 1, .meshes = {mesh}}); - if (!object) { - goto cleanup; - } - - state->scene = gfx_make_scene(); - SceneNode* node = gfx_make_object_node(object); - SceneNode* root = gfx_get_scene_root(state->scene); - gfx_set_node_parent(node, root); - - *app_state = state; - return true; - -cleanup: - if (state->gfx) { - gfx_destroy(&state->gfx); - } - free(state); - return false; -} - -static void shutdown(void* app_state) { - assert(app_state); - State* state = (State*)(app_state); - - if (state->app.state) { - assert(state->iso); - (*state->app.shutdown)(state->iso, state->app.state); - } - isogfx_del(&state->iso); - gfx_destroy(&state->gfx); - free(app_state); -} - -static void update(void* app_state, double t, double dt) { - assert(app_state); - State* state = (State*)(app_state); - - isogfx_update(state->iso, t); - - assert(state->app.update); - (*state->app.update)(state->iso, state->app.state, t, dt); -} - -static void render(void* app_state) { - assert(app_state); - State* state = (State*)(app_state); - - assert(state->app.render); - (*state->app.render)(state->iso, state->app.state); - - const Pixel* screen = isogfx_get_screen_buffer(state->iso); - assert(screen); - gfx_update_texture( - state->screen_texture, &(TextureDataDesc){.pixels = screen}); - - RenderBackend* render_backend = gfx_get_render_backend(state->gfx); - Renderer* renderer = gfx_get_renderer(state->gfx); - - gfx_start_frame(render_backend); - gfx_render_scene( - renderer, &(RenderSceneParams){ - .mode = RenderDefault, .scene = state->scene, .camera = 0}); - gfx_end_frame(render_backend); -} - -static void resize(void* app_state, int width, int height) { - assert(app_state); - State* state = (State*)(app_state); - - RenderBackend* render_backend = gfx_get_render_backend(state->gfx); - gfx_set_viewport(render_backend, width, height); -} - -int main(int argc, const char** argv) { - const int initial_width = SCREEN_WIDTH; - const int initial_height = SCREEN_HEIGHT; - const int max_fps = 60; - - gfx_app_run( - &(GfxAppDesc){ - .argc = argc, - .argv = argv, - .width = initial_width, - .height = initial_height, - .max_fps = max_fps, - .update_delta_time = max_fps > 0 ? 1.0 / (double)max_fps : 0.0, - .title = "Isometric Renderer"}, - &(GfxAppCallbacks){ - .init = init, - .update = update, - .render = render, - .resize = resize, - .shutdown = shutdown}); - - return 0; -} diff --git a/gfx-iso/asset/mkasset.py b/gfx-iso/asset/mkasset.py deleted file mode 100644 index 3ca8a1d..0000000 --- a/gfx-iso/asset/mkasset.py +++ /dev/null @@ -1,324 +0,0 @@ -# Converts assets to binary formats (.ts, .tm, .ss) for the engine. -# -# Input file formats: -# - Tiled tile set (.tsx) -# - Tiled tile map (.tmx) -# - Sprite sheets (.jpg, .png, etc), 1 row per animation. -# -# Output file formats: -# - Binary tile set file (.ts) -# - Binary tile map file (.tm) -# - Binary sprite sheet file (.ss) -# -import argparse -import ctypes -import os -from PIL import Image -import sys -from xml.etree import ElementTree - -# Maximum length of path strings in .TS and .TM files. -# Must match the engine's value. -MAX_PATH_LENGTH = 128 - - -def drop_extension(filepath): - return filepath[:filepath.rfind('.')] - - -def to_char_array(string, length): - """Convert a string to a fixed-length ASCII char array. - - The length of str must be at most length-1 so that the resulting string can - be null-terminated. - """ - assert (len(string) < length) - chars = string.encode("ascii") - nulls = ("\0" * (length - len(string))).encode("ascii") - return chars + nulls - - -def convert_tsx(input_filepath, output_filepath): - """Converts a Tiled .tsx tileset file to a .TS tile set file.""" - xml = ElementTree.parse(input_filepath) - root = xml.getroot() - - tile_count = int(root.attrib["tilecount"]) - max_tile_width = int(root.attrib["tilewidth"]) - max_tile_height = int(root.attrib["tileheight"]) - - print(f"Tile count: {tile_count}") - print(f"Max width: {max_tile_width}") - print(f"Max height: {max_tile_height}") - - with open(output_filepath, 'bw') as output: - output.write(ctypes.c_uint16(tile_count)) - output.write(ctypes.c_uint16(max_tile_width)) - output.write(ctypes.c_uint16(max_tile_height)) - - num_tile = 0 - for tile in root: - # Skip the "grid" and other non-tile elements. - if not tile.tag == "tile": - continue - - # Assuming tiles are numbered 0..N. - tile_id = int(tile.attrib["id"]) - assert (tile_id == num_tile) - num_tile += 1 - - image = tile[0] - tile_width = int(image.attrib["width"]) - tile_height = int(image.attrib["height"]) - tile_path = image.attrib["source"] - - output.write(ctypes.c_uint16(tile_width)) - output.write(ctypes.c_uint16(tile_height)) - - with Image.open(tile_path) as im: - bytes = im.convert('RGBA').tobytes() - output.write(bytes) - - -def convert_tmx(input_filepath, output_filepath): - """Converts a Tiled .tmx file to a .TM tile map file.""" - xml = ElementTree.parse(input_filepath) - root = xml.getroot() - - map_width = int(root.attrib["width"]) - map_height = int(root.attrib["height"]) - base_tile_width = int(root.attrib["tilewidth"]) - base_tile_height = int(root.attrib["tileheight"]) - num_layers = 1 - - print(f"Map width: {map_width}") - print(f"Map height: {map_height}") - print(f"Tile width: {base_tile_width}") - print(f"Tile height: {base_tile_height}") - - with open(output_filepath, 'bw') as output: - output.write(ctypes.c_uint16(map_width)) - output.write(ctypes.c_uint16(map_height)) - output.write(ctypes.c_uint16(base_tile_width)) - output.write(ctypes.c_uint16(base_tile_height)) - output.write(ctypes.c_uint16(num_layers)) - - tileset_path = None - - for child in root: - if child.tag == "tileset": - tileset = child - tileset_path = tileset.attrib["source"] - - print(f"Tile set: {tileset_path}") - - tileset_path = tileset_path.replace("tsx", "ts") - elif child.tag == "layer": - layer = child - layer_id = int(layer.attrib["id"]) - layer_width = int(layer.attrib["width"]) - layer_height = int(layer.attrib["height"]) - - print(f"Layer: {layer_id}") - print(f"Width: {layer_width}") - print(f"Height: {layer_height}") - - assert (tileset_path) - output.write(to_char_array(tileset_path, MAX_PATH_LENGTH)) - - # Assume the layer's dimensions matches the map's. - assert (layer_width == map_width) - assert (layer_height == map_height) - - data = layer[0] - # Handle other encodings later. - assert (data.attrib["encoding"] == "csv") - - csv = data.text.strip() - rows = csv.split('\n') - for row in rows: - tile_ids = [x.strip() for x in row.split(',') if x] - for tile_id in tile_ids: - output.write(ctypes.c_uint16(int(tile_id))) - - -def get_num_cols(image, sprite_width): - """Return the number of non-empty columns in the image. - - Assumes no gaps in the columns. - """ - assert (image.width % sprite_width == 0) - num_cols = image.width // sprite_width - - # Start the search from right to left. - for col in reversed(range(1, num_cols)): - left = (col - 1) * sprite_width - right = col * sprite_width - rect = image.crop((left, 0, right, image.height)) - min_max = rect.getextrema() - for (channel_min, channel_max) in min_max: - if channel_min != 0 or channel_max != 0: - # 'col' is the rightmost non-empty column. - # Assuming no gaps, col+1 is the number of non-empty columns. - return col + 1 - - return 0 - - -def get_sprite_sheet_rows(im, sprite_width, sprite_height): - """Gets the individual rows of a sprite sheet. - - The input sprite sheet can have any number of rows. - - Returns a list of lists [[sprite]], one inner list for the columns in each - row. - """ - # Sprite sheet's width and height must be integer multiples of the - # sprite's width and height. - assert (im.width % sprite_width == 0) - assert (im.height % sprite_height == 0) - - num_rows = im.height // sprite_height - - rows = [] - for row in range(num_rows): - # Get the number of columns. - upper = row * sprite_height - lower = (row + 1) * sprite_height - whole_row = im.crop((0, upper, im.width, lower)) - num_cols = get_num_cols(whole_row, sprite_width) - assert (num_cols > 0) - - # Crop the row into N columns. - cols = [] - for i in range(num_cols): - left = i * sprite_width - right = (i + 1) * sprite_width - sprite = im.crop((left, upper, right, lower)) - cols.append(sprite) - - assert (len(cols) == num_cols) - rows.append(cols) - - return rows - - -def make_image_from_rows(rows, sprite_width, sprite_height): - """Concatenate the rows into a single RGBA image.""" - im_width = sprite_width * max(len(row) for row in rows) - im_height = len(rows) * sprite_height - im = Image.new('RGBA', (im_width, im_height)) - y = 0 - for row in rows: - x = 0 - for sprite in row: - im.paste(sprite.convert('RGBA'), (x, y)) - x += sprite_width - y += sprite_height - return im - - -def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height, - output_filepath): - """Converts a set of sprite sheet images into a binary sprite sheet file - (.ss). - - The input sprite sheets can have any number of rows, one row per animation. - All rows from all sprite sheets are concatenated in the output file. - - The sprite's width and height is assumed constant throughout the input - sprite sheets. - """ - rows = [] - for input_filepath in input_file_paths: - with Image.open(input_filepath) as sprite_sheet: - rows.extend( - get_sprite_sheet_rows(sprite_sheet, sprite_width, - sprite_height)) - - im = make_image_from_rows(rows, sprite_width, sprite_height) - im = im.convert(mode="P", palette=Image.ADAPTIVE, colors=256) - - # The sprite data in 'rows' is no longer needed. - # Keep just the number of columns per row. - rows = [len(row) for row in rows] - - with open(output_filepath, 'bw') as output: - output.write(ctypes.c_uint16(sprite_width)) - output.write(ctypes.c_uint16(sprite_height)) - output.write(ctypes.c_uint16(len(rows))) - - # Write palette. - # getpalette() returns 256 colors, but the palette might use less than - # that. getcolors() returns the number of unique colors. - # getpalette() also returns a flattened list, which is why we must *4. - num_colours = len(im.getcolors()) - colours = im.getpalette(rawmode="RGBA")[:4 * num_colours] - palette = [] - for i in range(0, 4 * num_colours, 4): - palette.append((colours[i], colours[i + 1], colours[i + 2], - colours[i + 3])) - - output.write(ctypes.c_uint16(len(palette))) - output.write(bytearray(colours)) - - print(f"Sprite width: {sprite_width}") - print(f"Sprite height: {sprite_height}") - print(f"Rows: {len(rows)}") - print(f"Colours: {len(palette)}") - - # print("Palette") - # for i, colour in enumerate(palette): - # print(f"{i}: {colour}") - - for row, num_columns in enumerate(rows): - output.write(ctypes.c_uint16(num_columns)) - upper = row * sprite_height - lower = (row + 1) * sprite_height - for col in range(num_columns): - left = col * sprite_width - right = (col + 1) * sprite_width - sprite = im.crop((left, upper, right, lower)) - sprite_bytes = sprite.tobytes() - - assert (len(sprite_bytes) == sprite_width * sprite_height) - output.write(sprite_bytes) - - # if (row == 0) and (col == 0): - # print(f"Sprite: ({len(sprite_bytes)})") - # print(list(sprite_bytes)) - # sprite.save("out.png") - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("input", - nargs="+", - help="Input file (.tsx, .tmx) or path regex (sprite sheets)") - parser.add_argument("--width", type=int, help="Sprite width in pixels") - parser.add_argument("--height", type=int, help="Sprite height in pixels") - parser.add_argument("--out", help="Output file (sprite sheets)") - args = parser.parse_args() - - if ".tsx" in args.input: - output_filepath_no_ext = drop_extension(args.input) - output_filepath = output_filepath_no_ext + ".ts" - convert_tsx(args.input, output_filepath) - elif ".tmx" in args.input: - output_filepath_no_ext = drop_extension(args.input) - output_filepath = output_filepath_no_ext + ".tm" - convert_tmx(args.input, output_filepath) - else: - # Sprite sheets. - if not args.width or not args.height: - print("Sprite width and height must be given") - return 1 - output_filepath = args.out if args.out else "out.ss" - convert_sprite_sheet(args.input, args.width, args.height, - output_filepath) - - return 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/gfx-iso/demos/CMakeLists.txt b/gfx-iso/demos/CMakeLists.txt new file mode 100644 index 0000000..c0a4101 --- /dev/null +++ b/gfx-iso/demos/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(checkerboard) +add_subdirectory(isomap) diff --git a/gfx-iso/demos/checkerboard/CMakeLists.txt b/gfx-iso/demos/checkerboard/CMakeLists.txt new file mode 100644 index 0000000..f178262 --- /dev/null +++ b/gfx-iso/demos/checkerboard/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.0) + +project(checkerboard) + +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED On) +set(CMAKE_C_EXTENSIONS Off) + +add_executable(checkerboard + checkerboard.c) + +target_link_libraries(checkerboard PRIVATE + isogfx-app) + +target_compile_options(checkerboard PRIVATE -Wall -Wextra -Wpedantic) diff --git a/gfx-iso/demos/checkerboard/checkerboard.c b/gfx-iso/demos/checkerboard/checkerboard.c new file mode 100644 index 0000000..9730aea --- /dev/null +++ b/gfx-iso/demos/checkerboard/checkerboard.c @@ -0,0 +1,114 @@ +#include +#include + +#include +#include +#include + +static const int TILE_WIDTH = 64; +static const int TILE_HEIGHT = TILE_WIDTH / 2; +static const int WORLD_WIDTH = 20; +static const int WORLD_HEIGHT = 20; + +static const TileDesc tile_set[] = { + {.type = TileFromColour, + .width = TILE_WIDTH, + .height = TILE_HEIGHT, + .colour = (Pixel){.r = 0x38, .g = 0x3b, .b = 0x46, .a = 0xff}}, + {.type = TileFromColour, + .width = TILE_WIDTH, + .height = TILE_HEIGHT, + .colour = (Pixel){.r = 0xA5, .g = 0xb3, .b = 0xc0, .a = 0xff}}, + {.type = TileFromColour, + .width = TILE_WIDTH, + .height = TILE_HEIGHT, + .colour = (Pixel){.r = 0xdc, .g = 0x76, .b = 0x84, .a = 0xff}}, +}; + +typedef enum Colour { + Black, + White, + Red, +} Colour; + +typedef struct IsoGfxAppState { + Tile red; + int xpick; + int ypick; +} IsoGfxAppState; + +static void make_checkerboard(IsoGfx* iso, Tile black, Tile white) { + assert(iso); + for (int y = 0; y < isogfx_world_height(iso); ++y) { + for (int x = 0; x < isogfx_world_width(iso); ++x) { + const int odd_col = x & 1; + const int odd_row = y & 1; + const Tile value = (odd_row ^ odd_col) == 0 ? black : white; + isogfx_set_tile(iso, x, y, value); + } + } +} + +static bool init( + IsoGfxAppState* state, IsoGfx* iso, int argc, const char** argv) { + assert(state); + assert(iso); + + if (!isogfx_make_world( + iso, &(WorldDesc){ + .tile_width = TILE_WIDTH, + .tile_height = TILE_HEIGHT, + .world_width = WORLD_WIDTH, + .world_height = WORLD_HEIGHT})) { + return false; + } + + const Tile black = isogfx_make_tile(iso, &tile_set[Black]); + const Tile white = isogfx_make_tile(iso, &tile_set[White]); + state->red = isogfx_make_tile(iso, &tile_set[Red]); + make_checkerboard(iso, black, white); + + return true; +} + +static void shutdown(IsoGfxAppState* state, IsoGfx* iso) { + assert(state); + assert(iso); +} + +static void update(IsoGfxAppState* state, IsoGfx* iso, double t, double dt) { + assert(state); + assert(iso); + + double mouse_x, mouse_y; + gfx_app_get_mouse_position(&mouse_x, &mouse_y); + + isogfx_pick_tile(iso, mouse_x, mouse_y, &state->xpick, &state->ypick); + + printf("Picked tile: (%d, %d)\n", state->xpick, state->ypick); +} + +static void render(IsoGfxAppState* state, IsoGfx* iso) { + assert(state); + assert(iso); + + isogfx_render(iso); + + if ((state->xpick != -1) && (state->ypick != -1)) { + isogfx_draw_tile(iso, state->xpick, state->ypick, state->red); + } +} + +int main(int argc, const char** argv) { + IsoGfxAppState state = {0}; + iso_run( + argc, argv, + &(IsoGfxApp){ + .state = &state, + .init = init, + .shutdown = shutdown, + .update = update, + .render = render, + }); + return 0; +} diff --git a/gfx-iso/demos/isomap/CMakeLists.txt b/gfx-iso/demos/isomap/CMakeLists.txt new file mode 100644 index 0000000..13edcc7 --- /dev/null +++ b/gfx-iso/demos/isomap/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.0) + +project(isomap) + +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED On) +set(CMAKE_C_EXTENSIONS Off) + +add_executable(isomap + isomap.c) + +target_link_libraries(isomap PRIVATE + isogfx-app) + +target_compile_options(isomap PRIVATE -Wall -Wextra -Wpedantic) diff --git a/gfx-iso/demos/isomap/isomap.c b/gfx-iso/demos/isomap/isomap.c new file mode 100644 index 0000000..d204d28 --- /dev/null +++ b/gfx-iso/demos/isomap/isomap.c @@ -0,0 +1,72 @@ +#include +#include + +#include +#include + +typedef struct IsoGfxAppState { + int xpick; + int ypick; + SpriteSheet stag_sheet; + Sprite stag; +} IsoGfxAppState; + +static bool init( + IsoGfxAppState* state, IsoGfx* iso, int argc, const char** argv) { + assert(state); + assert(iso); + + if (!isogfx_load_world(iso, "/home/jeanne/assets/tilemaps/demo1.tm")) { + return false; + } + + if (!isogfx_load_sprite_sheet( + iso, "/home/jeanne/assets/tilesets/scrabling/critters/stag/stag.ss", + &state->stag_sheet)) { + return false; + } + + state->stag = isogfx_make_sprite(iso, state->stag_sheet); + isogfx_set_sprite_position(iso, state->stag, 5, 4); + + return true; +} + +static void shutdown(IsoGfxAppState* state, IsoGfx* iso) { + assert(state); + assert(iso); +} + +static void update(IsoGfxAppState* state, IsoGfx* iso, double t, double dt) { + assert(state); + assert(iso); + + double mouse_x, mouse_y; + gfx_app_get_mouse_position(&mouse_x, &mouse_y); + + isogfx_pick_tile(iso, mouse_x, mouse_y, &state->xpick, &state->ypick); + + // printf("Picked tile: (%d, %d)\n", state->xpick, state->ypick); +} + +static void render(IsoGfxAppState* state, IsoGfx* iso) { + assert(state); + assert(iso); + + isogfx_render(iso); +} + +int main(int argc, const char** argv) { + IsoGfxAppState state = {0}; + iso_run( + argc, argv, + &(IsoGfxApp){ + .pixel_scale = 2, + .state = &state, + .init = init, + .shutdown = shutdown, + .update = update, + .render = render, + }); + return 0; +} diff --git a/gfx-iso/include/isogfx/app.h b/gfx-iso/include/isogfx/app.h new file mode 100644 index 0000000..0a0fcc1 --- /dev/null +++ b/gfx-iso/include/isogfx/app.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include + +typedef struct IsoGfx IsoGfx; +typedef struct IsoGfxApp IsoGfxApp; + +typedef struct IsoGfxAppState IsoGfxAppState; + +typedef struct IsoGfxApp { + int pixel_scale; // 0 or 1 for 1:1 scale. + IsoGfxAppState* state; + + bool (*init)(IsoGfxAppState*, IsoGfx*, int argc, const char** argv); + void (*shutdown)(IsoGfxAppState*, IsoGfx*); + void (*update)(IsoGfxAppState*, IsoGfx*, double t, double dt); + void (*render)(IsoGfxAppState*, IsoGfx*); +} IsoGfxApp; + +void iso_run(int argc, const char** argv, IsoGfxApp*); diff --git a/gfx-iso/src/app.c b/gfx-iso/src/app.c new file mode 100644 index 0000000..079ac96 --- /dev/null +++ b/gfx-iso/src/app.c @@ -0,0 +1,198 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static const int SCREEN_WIDTH = 1408; +static const int SCREEN_HEIGHT = 960; +static const int MAX_FPS = 60; + +typedef struct AppState { + Gfx* gfx; + IsoGfx* iso; + IsoGfxApp* app; + Texture* screen_texture; + Scene* scene; +} AppState; + +typedef struct GfxAppState { + AppState state; +} GfxAppState; + +static bool init(GfxAppState* gfx_app_state, int argc, const char** argv) { + assert(gfx_app_state); + AppState* state = &gfx_app_state->state; + + IsoGfxApp* app = state->app; + + if (!(state->iso = isogfx_new(&(IsoGfxDesc){ + .screen_width = SCREEN_WIDTH, .screen_height = SCREEN_HEIGHT}))) { + goto cleanup; + } + + if (!(*app->init)(app->state, state->iso, argc, argv)) { + goto cleanup; + } + + // Apply pixel scaling if requested by the app. + int texture_width, texture_height; + if (app->pixel_scale > 1) { + texture_width = SCREEN_WIDTH / app->pixel_scale; + texture_height = SCREEN_HEIGHT / app->pixel_scale; + isogfx_resize(state->iso, texture_width, texture_height); + } else { + texture_width = SCREEN_WIDTH; + texture_height = SCREEN_HEIGHT; + } + + if (!(state->gfx = gfx_init())) { + goto cleanup; + } + RenderBackend* render_backend = gfx_get_render_backend(state->gfx); + + if (!(state->screen_texture = gfx_make_texture( + render_backend, &(TextureDesc){ + .width = texture_width, + .height = texture_height, + .dimension = Texture2D, + .format = TextureSRGBA8, + .filtering = NearestFiltering, + .wrap = ClampToEdge, + .mipmaps = false}))) { + goto cleanup; + } + + ShaderProgram* shader = gfx_make_view_texture_shader(render_backend); + if (!shader) { + goto cleanup; + } + + Geometry* geometry = gfx_make_quad_11(render_backend); + if (!geometry) { + goto cleanup; + } + + MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1}; + material_desc.uniforms[0] = (ShaderUniform){ + .type = UniformTexture, + .value.texture = state->screen_texture, + .name = sstring_make("Texture")}; + Material* material = gfx_make_material(&material_desc); + if (!material) { + return false; + } + + const MeshDesc mesh_desc = + (MeshDesc){.geometry = geometry, .material = material, .shader = shader}; + Mesh* mesh = gfx_make_mesh(&mesh_desc); + if (!mesh) { + goto cleanup; + } + + SceneObject* object = + gfx_make_object(&(ObjectDesc){.num_meshes = 1, .meshes = {mesh}}); + if (!object) { + goto cleanup; + } + + state->scene = gfx_make_scene(); + SceneNode* node = gfx_make_object_node(object); + SceneNode* root = gfx_get_scene_root(state->scene); + gfx_set_node_parent(node, root); + + return true; + +cleanup: + if (state->gfx) { + gfx_destroy(&state->gfx); + } + free(state); + return false; +} + +static void shutdown(GfxAppState* gfx_app_state) { + assert(gfx_app_state); + AppState* state = &gfx_app_state->state; + + if (state->app) { + assert(state->iso); + (*state->app->shutdown)(state->app->state, state->iso); + } + + isogfx_del(&state->iso); + gfx_destroy(&state->gfx); +} + +static void update(GfxAppState* gfx_app_state, double t, double dt) { + assert(gfx_app_state); + AppState* state = &gfx_app_state->state; + + isogfx_update(state->iso, t); + + assert(state->app->update); + (*state->app->update)(state->app->state, state->iso, t, dt); +} + +static void render(GfxAppState* gfx_app_state) { + assert(gfx_app_state); + AppState* state = &gfx_app_state->state; + + assert(state->app->render); + (*state->app->render)(state->app->state, state->iso); + + const Pixel* screen = isogfx_get_screen_buffer(state->iso); + assert(screen); + gfx_update_texture( + state->screen_texture, &(TextureDataDesc){.pixels = screen}); + + RenderBackend* render_backend = gfx_get_render_backend(state->gfx); + Renderer* renderer = gfx_get_renderer(state->gfx); + + gfx_start_frame(render_backend); + gfx_render_scene( + renderer, &(RenderSceneParams){ + .mode = RenderDefault, .scene = state->scene, .camera = 0}); + gfx_end_frame(render_backend); +} + +static void resize(GfxAppState* gfx_app_state, int width, int height) { + assert(gfx_app_state); + AppState* state = &gfx_app_state->state; + + RenderBackend* render_backend = gfx_get_render_backend(state->gfx); + gfx_set_viewport(render_backend, width, height); +} + +void iso_run(int argc, const char** argv, IsoGfxApp* app) { + GfxAppState app_state = { + .state = (AppState){ + .app = app, + } + }; + gfx_app_run( + &(GfxAppDesc){ + .argc = argc, + .argv = argv, + .width = SCREEN_WIDTH, + .height = SCREEN_HEIGHT, + .max_fps = MAX_FPS, + .update_delta_time = MAX_FPS > 0 ? 1.0 / (double)MAX_FPS : 0.0, + .title = "Isometric Renderer", + .app_state = &app_state}, + &(GfxAppCallbacks){ + .init = init, + .update = update, + .render = render, + .resize = resize, + .shutdown = shutdown}); +} diff --git a/gfx-iso/tools/mkasset.py b/gfx-iso/tools/mkasset.py new file mode 100644 index 0000000..3ca8a1d --- /dev/null +++ b/gfx-iso/tools/mkasset.py @@ -0,0 +1,324 @@ +# Converts assets to binary formats (.ts, .tm, .ss) for the engine. +# +# Input file formats: +# - Tiled tile set (.tsx) +# - Tiled tile map (.tmx) +# - Sprite sheets (.jpg, .png, etc), 1 row per animation. +# +# Output file formats: +# - Binary tile set file (.ts) +# - Binary tile map file (.tm) +# - Binary sprite sheet file (.ss) +# +import argparse +import ctypes +import os +from PIL import Image +import sys +from xml.etree import ElementTree + +# Maximum length of path strings in .TS and .TM files. +# Must match the engine's value. +MAX_PATH_LENGTH = 128 + + +def drop_extension(filepath): + return filepath[:filepath.rfind('.')] + + +def to_char_array(string, length): + """Convert a string to a fixed-length ASCII char array. + + The length of str must be at most length-1 so that the resulting string can + be null-terminated. + """ + assert (len(string) < length) + chars = string.encode("ascii") + nulls = ("\0" * (length - len(string))).encode("ascii") + return chars + nulls + + +def convert_tsx(input_filepath, output_filepath): + """Converts a Tiled .tsx tileset file to a .TS tile set file.""" + xml = ElementTree.parse(input_filepath) + root = xml.getroot() + + tile_count = int(root.attrib["tilecount"]) + max_tile_width = int(root.attrib["tilewidth"]) + max_tile_height = int(root.attrib["tileheight"]) + + print(f"Tile count: {tile_count}") + print(f"Max width: {max_tile_width}") + print(f"Max height: {max_tile_height}") + + with open(output_filepath, 'bw') as output: + output.write(ctypes.c_uint16(tile_count)) + output.write(ctypes.c_uint16(max_tile_width)) + output.write(ctypes.c_uint16(max_tile_height)) + + num_tile = 0 + for tile in root: + # Skip the "grid" and other non-tile elements. + if not tile.tag == "tile": + continue + + # Assuming tiles are numbered 0..N. + tile_id = int(tile.attrib["id"]) + assert (tile_id == num_tile) + num_tile += 1 + + image = tile[0] + tile_width = int(image.attrib["width"]) + tile_height = int(image.attrib["height"]) + tile_path = image.attrib["source"] + + output.write(ctypes.c_uint16(tile_width)) + output.write(ctypes.c_uint16(tile_height)) + + with Image.open(tile_path) as im: + bytes = im.convert('RGBA').tobytes() + output.write(bytes) + + +def convert_tmx(input_filepath, output_filepath): + """Converts a Tiled .tmx file to a .TM tile map file.""" + xml = ElementTree.parse(input_filepath) + root = xml.getroot() + + map_width = int(root.attrib["width"]) + map_height = int(root.attrib["height"]) + base_tile_width = int(root.attrib["tilewidth"]) + base_tile_height = int(root.attrib["tileheight"]) + num_layers = 1 + + print(f"Map width: {map_width}") + print(f"Map height: {map_height}") + print(f"Tile width: {base_tile_width}") + print(f"Tile height: {base_tile_height}") + + with open(output_filepath, 'bw') as output: + output.write(ctypes.c_uint16(map_width)) + output.write(ctypes.c_uint16(map_height)) + output.write(ctypes.c_uint16(base_tile_width)) + output.write(ctypes.c_uint16(base_tile_height)) + output.write(ctypes.c_uint16(num_layers)) + + tileset_path = None + + for child in root: + if child.tag == "tileset": + tileset = child + tileset_path = tileset.attrib["source"] + + print(f"Tile set: {tileset_path}") + + tileset_path = tileset_path.replace("tsx", "ts") + elif child.tag == "layer": + layer = child + layer_id = int(layer.attrib["id"]) + layer_width = int(layer.attrib["width"]) + layer_height = int(layer.attrib["height"]) + + print(f"Layer: {layer_id}") + print(f"Width: {layer_width}") + print(f"Height: {layer_height}") + + assert (tileset_path) + output.write(to_char_array(tileset_path, MAX_PATH_LENGTH)) + + # Assume the layer's dimensions matches the map's. + assert (layer_width == map_width) + assert (layer_height == map_height) + + data = layer[0] + # Handle other encodings later. + assert (data.attrib["encoding"] == "csv") + + csv = data.text.strip() + rows = csv.split('\n') + for row in rows: + tile_ids = [x.strip() for x in row.split(',') if x] + for tile_id in tile_ids: + output.write(ctypes.c_uint16(int(tile_id))) + + +def get_num_cols(image, sprite_width): + """Return the number of non-empty columns in the image. + + Assumes no gaps in the columns. + """ + assert (image.width % sprite_width == 0) + num_cols = image.width // sprite_width + + # Start the search from right to left. + for col in reversed(range(1, num_cols)): + left = (col - 1) * sprite_width + right = col * sprite_width + rect = image.crop((left, 0, right, image.height)) + min_max = rect.getextrema() + for (channel_min, channel_max) in min_max: + if channel_min != 0 or channel_max != 0: + # 'col' is the rightmost non-empty column. + # Assuming no gaps, col+1 is the number of non-empty columns. + return col + 1 + + return 0 + + +def get_sprite_sheet_rows(im, sprite_width, sprite_height): + """Gets the individual rows of a sprite sheet. + + The input sprite sheet can have any number of rows. + + Returns a list of lists [[sprite]], one inner list for the columns in each + row. + """ + # Sprite sheet's width and height must be integer multiples of the + # sprite's width and height. + assert (im.width % sprite_width == 0) + assert (im.height % sprite_height == 0) + + num_rows = im.height // sprite_height + + rows = [] + for row in range(num_rows): + # Get the number of columns. + upper = row * sprite_height + lower = (row + 1) * sprite_height + whole_row = im.crop((0, upper, im.width, lower)) + num_cols = get_num_cols(whole_row, sprite_width) + assert (num_cols > 0) + + # Crop the row into N columns. + cols = [] + for i in range(num_cols): + left = i * sprite_width + right = (i + 1) * sprite_width + sprite = im.crop((left, upper, right, lower)) + cols.append(sprite) + + assert (len(cols) == num_cols) + rows.append(cols) + + return rows + + +def make_image_from_rows(rows, sprite_width, sprite_height): + """Concatenate the rows into a single RGBA image.""" + im_width = sprite_width * max(len(row) for row in rows) + im_height = len(rows) * sprite_height + im = Image.new('RGBA', (im_width, im_height)) + y = 0 + for row in rows: + x = 0 + for sprite in row: + im.paste(sprite.convert('RGBA'), (x, y)) + x += sprite_width + y += sprite_height + return im + + +def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height, + output_filepath): + """Converts a set of sprite sheet images into a binary sprite sheet file + (.ss). + + The input sprite sheets can have any number of rows, one row per animation. + All rows from all sprite sheets are concatenated in the output file. + + The sprite's width and height is assumed constant throughout the input + sprite sheets. + """ + rows = [] + for input_filepath in input_file_paths: + with Image.open(input_filepath) as sprite_sheet: + rows.extend( + get_sprite_sheet_rows(sprite_sheet, sprite_width, + sprite_height)) + + im = make_image_from_rows(rows, sprite_width, sprite_height) + im = im.convert(mode="P", palette=Image.ADAPTIVE, colors=256) + + # The sprite data in 'rows' is no longer needed. + # Keep just the number of columns per row. + rows = [len(row) for row in rows] + + with open(output_filepath, 'bw') as output: + output.write(ctypes.c_uint16(sprite_width)) + output.write(ctypes.c_uint16(sprite_height)) + output.write(ctypes.c_uint16(len(rows))) + + # Write palette. + # getpalette() returns 256 colors, but the palette might use less than + # that. getcolors() returns the number of unique colors. + # getpalette() also returns a flattened list, which is why we must *4. + num_colours = len(im.getcolors()) + colours = im.getpalette(rawmode="RGBA")[:4 * num_colours] + palette = [] + for i in range(0, 4 * num_colours, 4): + palette.append((colours[i], colours[i + 1], colours[i + 2], + colours[i + 3])) + + output.write(ctypes.c_uint16(len(palette))) + output.write(bytearray(colours)) + + print(f"Sprite width: {sprite_width}") + print(f"Sprite height: {sprite_height}") + print(f"Rows: {len(rows)}") + print(f"Colours: {len(palette)}") + + # print("Palette") + # for i, colour in enumerate(palette): + # print(f"{i}: {colour}") + + for row, num_columns in enumerate(rows): + output.write(ctypes.c_uint16(num_columns)) + upper = row * sprite_height + lower = (row + 1) * sprite_height + for col in range(num_columns): + left = col * sprite_width + right = (col + 1) * sprite_width + sprite = im.crop((left, upper, right, lower)) + sprite_bytes = sprite.tobytes() + + assert (len(sprite_bytes) == sprite_width * sprite_height) + output.write(sprite_bytes) + + # if (row == 0) and (col == 0): + # print(f"Sprite: ({len(sprite_bytes)})") + # print(list(sprite_bytes)) + # sprite.save("out.png") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("input", + nargs="+", + help="Input file (.tsx, .tmx) or path regex (sprite sheets)") + parser.add_argument("--width", type=int, help="Sprite width in pixels") + parser.add_argument("--height", type=int, help="Sprite height in pixels") + parser.add_argument("--out", help="Output file (sprite sheets)") + args = parser.parse_args() + + if ".tsx" in args.input: + output_filepath_no_ext = drop_extension(args.input) + output_filepath = output_filepath_no_ext + ".ts" + convert_tsx(args.input, output_filepath) + elif ".tmx" in args.input: + output_filepath_no_ext = drop_extension(args.input) + output_filepath = output_filepath_no_ext + ".tm" + convert_tmx(args.input, output_filepath) + else: + # Sprite sheets. + if not args.width or not args.height: + print("Sprite width and height must be given") + return 1 + output_filepath = args.out if args.out else "out.ss" + convert_sprite_sheet(args.input, args.width, args.height, + output_filepath) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) -- cgit v1.2.3