Merge branch 'main' into break-literally-everything

This commit is contained in:
2026-03-03 12:28:48 -06:00
47 changed files with 851 additions and 346 deletions

View File

@@ -9,7 +9,6 @@ on:
- main
jobs:
run-tests:
runs-on: ubuntu-latest
steps:

View File

@@ -84,6 +84,11 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
# Add main code
add_subdirectory(${DUSK_SOURCES_DIR})
# Include generated headers
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${DUSK_GENERATED_HEADERS_DIR}
)
# Handle tests
if(ENABLE_TESTS)
enable_testing()

View File

@@ -187,7 +187,6 @@ function sceneRender()
if INPUT_POINTER then
mouseX = inputGetValue(INPUT_ACTION_POINTERX) * screenGetWidth()
mouseY = inputGetValue(INPUT_ACTION_POINTERY) * screenGetHeight()
end
-- Draw cursor
spriteBatchPush(
@@ -198,8 +197,10 @@ function sceneRender()
0, 0,
1, 1
)
end
textDraw(10, 10, "Minesweeper")
textDraw(10, 10, "Hello World")
-- centerX = math.floor(screenGetWidth() / 2)
-- centerY = math.floor(screenGetHeight() / 2)

Binary file not shown.

BIN
assets/ui/minogram.dtf Normal file

Binary file not shown.

View File

@@ -0,0 +1,63 @@
message(FATAL_ERROR "Configure Dolphin")
if(DUSK_TARGET_SYSTEM STREQUAL "gamecube" OR DUSK_TARGET_SYSTEM STREQUAL "wii")
# Override to make library and binary be the same.
set(DUSK_LIBRARY_TARGET_NAME "${DUSK_LIBRARY_TARGET_NAME}.elf" CACHE INTERNAL ${DUSK_CACHE_TARGET})
endif()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions")
# configure_file(opengl.pc.in opengl.pc @ONLY)
find_package(PkgConfig REQUIRED)
pkg_check_modules(zip IMPORTED_TARGET libzip)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DOLPHIN
)
# Disable all warnings
target_compile_options(${DUSK_LIBRARY_TARGET_NAME} PRIVATE -w)
# Custom flags for cglm
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED)
# Compile lua
include(FetchContent)
FetchContent_Declare(
liblua
URL https://www.lua.org/ftp/lua-5.5.0.tar.gz
)
FetchContent_MakeAvailable(liblua)
set(LUA_SRC_DIR "${liblua_SOURCE_DIR}/src")
set(LUA_C_FILES
lapi.c lauxlib.c lbaselib.c lcode.c lcorolib.c lctype.c ldblib.c ldebug.c
ldo.c ldump.c lfunc.c lgc.c linit.c liolib.c llex.c lmathlib.c lmem.c
loadlib.c lobject.c lopcodes.c loslib.c lparser.c lstate.c lstring.c
lstrlib.c ltable.c ltablib.c ltm.c lundump.c lutf8lib.c lvm.c lzio.c
)
list(TRANSFORM LUA_C_FILES PREPEND "${LUA_SRC_DIR}/")
add_library(liblua STATIC ${LUA_C_FILES})
target_include_directories(liblua PUBLIC "${LUA_SRC_DIR}")
target_compile_definitions(liblua PRIVATE LUA_USE_C89)
add_library(lua::lua ALIAS liblua)
set(Lua_FOUND TRUE CACHE BOOL "Lua found" FORCE)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
cglm
liblua
m
fat
PkgConfig::zip
)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
DISPLAY_WINDOW_WIDTH_DEFAULT=640
DISPLAY_WINDOW_HEIGHT_DEFAULT=480
DISPLAY_WIDTH=640
DISPLAY_HEIGHT=480
DISPLAY_SIZE_DYNAMIC=0
INPUT_GAMEPAD=1
THREAD_PTHREAD=1
TIME_FIXED=1
)

View File

@@ -0,0 +1 @@
include(cmake/configure/gamecube.cmake)

View File

@@ -0,0 +1,26 @@
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
SDL2
pthread
OpenGL::GL
GL
m
)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
DISPLAY_SDL2=1
DISPLAY_WINDOW_WIDTH_DEFAULT=1080
DISPLAY_WINDOW_HEIGHT_DEFAULT=810
DISPLAY_SCREEN_HEIGHT_DEFAULT=270
DISPLAY_SHADER=1
INPUT_SDL2=1
INPUT_KEYBOARD=1
INPUT_POINTER=1
INPUT_GAMEPAD=1
THREAD_PTHREAD=1
TIME_SDL2=1
TIME_FIXED=0
)

36
cmake/configure/psp.cmake Normal file
View File

@@ -0,0 +1,36 @@
find_package(pspsdk REQUIRED)
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES}
SDL2
pthread
OpenGL::GL
zip
bz2
z
mbedtls
mbedcrypto
lzma
m
)
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
${SDL2_INCLUDE_DIRS}
)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
DISPLAY_SDL2=1
DISPLAY_WINDOW_WIDTH_DEFAULT=480
DISPLAY_WINDOW_HEIGHT_DEFAULT=272
DISPLAY_WIDTH=480
DISPLAY_HEIGHT=272
DISPLAY_SIZE_DYNAMIC=0
DISPLAY_COLOR_TABLE=1
INPUT_SDL2=1
INPUT_GAMEPAD=1
THREAD_PTHREAD=1
TIME_FIXED=1
)

View File

@@ -0,0 +1 @@
include(cmake/configure/dolphin.cmake)

View File

@@ -8,6 +8,7 @@
#include "asset/asset.h"
#include "assert/assert.h"
#include "map/mapchunk.h"
#include "util/endian.h"
#pragma pack(push, 1)
typedef struct {
@@ -56,7 +57,7 @@ errorret_t assetMapChunkHandler(assetcustom_t custom) {
}
// Fix endianess if necessary
header.tileCount = le32toh(header.tileCount);
header.tileCount = endianLittleToHost32(header.tileCount);
if(header.tileCount != CHUNK_TILE_COUNT) {
zip_fclose(custom.zipFile);
@@ -111,7 +112,7 @@ errorret_t assetMapChunkHandler(assetcustom_t custom) {
}
// Fix endianess if necessary
modelHeader.vertexCount = le32toh(modelHeader.vertexCount);
modelHeader.vertexCount = endianLittleToHost32(modelHeader.vertexCount);
if(
vertexIndex + modelHeader.vertexCount >

View File

@@ -7,7 +7,7 @@
#pragma once
#include "error/error.h"
#include "display/palette/palette.h"
#include "display/texture/palette.h"
typedef struct assetentire_s assetentire_t;

View File

@@ -8,7 +8,8 @@
#include "assettexture.h"
#include "asset/assettype.h"
#include "assert/assert.h"
#include "display/texture.h"
#include "display/texture/texture.h"
#include "util/endian.h"
errorret_t assetTextureLoad(assetentire_t entire) {
assertNotNull(entire.data, "Data pointer cannot be NULL.");
@@ -32,8 +33,8 @@ errorret_t assetTextureLoad(assetentire_t entire) {
}
// Fix endian
assetData->width = le32toh(assetData->width);
assetData->height = le32toh(assetData->height);
assetData->width = endianLittleToHost32(assetData->width);
assetData->height = endianLittleToHost32(assetData->height);
// Check dimensions.
if(

View File

@@ -8,8 +8,8 @@
#pragma once
#include "error/error.h"
#define ASSET_TEXTURE_WIDTH_MAX 256
#define ASSET_TEXTURE_HEIGHT_MAX 256
#define ASSET_TEXTURE_WIDTH_MAX 2048
#define ASSET_TEXTURE_HEIGHT_MAX 2048
#define ASSET_TEXTURE_SIZE_MAX ( \
ASSET_TEXTURE_WIDTH_MAX * ASSET_TEXTURE_HEIGHT_MAX \
)
@@ -20,10 +20,8 @@ typedef struct assetentire_s assetentire_t;
typedef struct {
char_t header[3];
uint8_t version;
uint32_t width;
uint32_t height;
uint8_t paletteIndex;
uint8_t palette[ASSET_TEXTURE_SIZE_MAX];
} assettexture_t;
#pragma pack(pop)

View File

@@ -7,7 +7,9 @@
#include "asset/asset.h"
#include "assert/assert.h"
#include "display/tileset/tileset.h"
#include "display/texture/tileset.h"
#include "util/memory.h"
#include "util/endian.h"
errorret_t assetTilesetLoad(assetentire_t entire) {
assertNotNull(entire.data, "Asset data cannot be null");
@@ -28,5 +30,41 @@ errorret_t assetTilesetLoad(assetentire_t entire) {
errorThrow("Unsupported tileset version");
}
errorThrow("unfinished");
// Fix endianness
tilesetData->tileWidth = endianLittleToHost16(tilesetData->tileWidth);
tilesetData->tileHeight = endianLittleToHost16(tilesetData->tileHeight);
tilesetData->columnCount = endianLittleToHost16(tilesetData->columnCount);
tilesetData->rowCount = endianLittleToHost16(tilesetData->rowCount);
tilesetData->right = endianLittleToHost16(tilesetData->right);
tilesetData->bottom = endianLittleToHost16(tilesetData->bottom);
if(tilesetData->tileWidth == 0) {
errorThrow("Tile width cannot be 0");
}
if(tilesetData->tileHeight == 0) {
errorThrow("Tile height cannot be 0");
}
if(tilesetData->columnCount == 0) {
errorThrow("Column count cannot be 0");
}
if(tilesetData->rowCount == 0) {
errorThrow("Row count cannot be 0");
}
tilesetData->u0 = endianLittleToHostFloat(tilesetData->u0);
tilesetData->v0 = endianLittleToHostFloat(tilesetData->v0);
if(tilesetData->v0 < 0.0f || tilesetData->v0 > 1.0f) {
errorThrow("Invalid v0 value in tileset");
}
// Setup tileset
tileset->tileWidth = tilesetData->tileWidth;
tileset->tileHeight = tilesetData->tileHeight;
tileset->tileCount = tilesetData->columnCount * tilesetData->rowCount;
tileset->columns = tilesetData->columnCount;
tileset->rows = tilesetData->rowCount;
tileset->uv[0] = tilesetData->u0;
tileset->uv[1] = tilesetData->v0;
errorOk();
}

View File

@@ -12,6 +12,14 @@
typedef struct {
char_t header[3];
uint8_t version;
uint16_t tileWidth;
uint16_t tileHeight;
uint16_t columnCount;
uint16_t rowCount;
uint16_t right;
uint16_t bottom;
float_t u0;
float_t v0;
} assettileset_t;
#pragma pack(pop)

View File

@@ -8,5 +8,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
debug.c
)
# Subdirs

View File

@@ -9,7 +9,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
display.c
framebuffer.c
screen.c
texture.c
spritebatch.c
text.c
)
@@ -17,38 +16,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Subdirectories
add_subdirectory(camera)
add_subdirectory(mesh)
add_subdirectory(palette)
add_subdirectory(tileset)
if(DUSK_TARGET_SYSTEM STREQUAL "linux")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
DISPLAY_SDL2=1
DISPLAY_WINDOW_WIDTH_DEFAULT=1080
DISPLAY_WINDOW_HEIGHT_DEFAULT=810
DISPLAY_SCREEN_HEIGHT_DEFAULT=270
)
elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
DISPLAY_SDL2=1
DISPLAY_WINDOW_WIDTH_DEFAULT=480
DISPLAY_WINDOW_HEIGHT_DEFAULT=272
DISPLAY_WIDTH=480
DISPLAY_HEIGHT=272
DISPLAY_SIZE_DYNAMIC=0
)
elseif(DUSK_TARGET_SYSTEM STREQUAL "gamecube" OR DUSK_TARGET_SYSTEM STREQUAL "wii")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
DISPLAY_WINDOW_WIDTH_DEFAULT=640
DISPLAY_WINDOW_HEIGHT_DEFAULT=480
DISPLAY_WIDTH=640
DISPLAY_HEIGHT=480
DISPLAY_SIZE_DYNAMIC=0
)
endif()
add_subdirectory(texture)
# Color definitions
dusk_run_python(
dusk_color_defs
tools.display.color.csv

View File

@@ -88,6 +88,7 @@ errorret_t displayInit(void) {
#else
GLint mask = 0;
glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &mask);
DISPLAY.usingShaderedPalettes = true;
if(mask & GL_CONTEXT_CORE_PROFILE_BIT) {
GLint numExtens = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &numExtens);
@@ -100,7 +101,7 @@ errorret_t displayInit(void) {
}
} else {
const char* ext = (const char*)glGetString(GL_EXTENSIONS);
DISPLAY.usingShaderedPalettes = (
DISPLAY.usingShaderedPalettes = !(
ext && strstr(ext, "GL_EXT_paletted_texture")
);
}

View File

@@ -6,7 +6,7 @@
*/
#pragma once
#include "display/texture.h"
#include "display/texture/texture.h"
#define FRAMEBUFFER_CLEAR_COLOR (1 << 0)
#define FRAMEBUFFER_CLEAR_DEPTH (1 << 1)

View File

@@ -7,7 +7,7 @@
#pragma once
#include "display/mesh/quad.h"
#include "display/texture.h"
#include "display/texture/texture.h"
#define SPRITEBATCH_SPRITES_MAX 128
#define SPRITEBATCH_VERTEX_COUNT (SPRITEBATCH_SPRITES_MAX * QUAD_VERTEX_COUNT)

View File

@@ -21,7 +21,7 @@ errorret_t textInit(void) {
}
void textDispose(void) {
// textureDispose(&DEFAULT_FONT_TEXTURE);
textureDispose(&DEFAULT_FONT_TEXTURE);
}
void textDrawChar(
@@ -36,7 +36,6 @@ void textDrawChar(
if(tileIndex < 0 || tileIndex >= tileset->tileCount) {
tileIndex = ((int32_t)'@') - TEXT_CHAR_START;
}
assertTrue(
tileIndex >= 0 && tileIndex <= tileset->tileCount,
"Character is out of bounds for font tiles"

View File

@@ -7,8 +7,8 @@
#pragma once
#include "asset/asset.h"
#include "display/texture.h"
#include "display/tileset/tileset.h"
#include "display/texture/texture.h"
#include "display/texture/tileset.h"
#define TEXT_CHAR_START '!'

View File

@@ -6,5 +6,7 @@
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
tileset.c
texture.c
palette.c
)

View File

@@ -8,6 +8,18 @@
#pragma once
#include "display/color.h"
#if DISPLAY_SDL2
#if DISPLAY_SHADER == 1
#elif DISPLAY_COLOR_TABLE == 1
#else
#error "Unsupported palette mode"
#endif
#else
#error "Unsupported palette mode"
#endif
#define PALETTE_COUNT_MAX 4
#define PALETTE_COLOR_COUNT_MAX 0xFF

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "palettetexture.h"
#include "assert/assert.h"
void paletteTextureInit(
palettetexture_t *texture,
const int32_t width,
const int32_t height,
const uint8_t *data
) {
assertNotNull(texture, "Palette texture cannot be NULL");
assertTrue(width > 0 && height > 0, "width/height must be greater than 0");
assertNotNull(data, "Palette texture data cannot be NULL");
#if DISPLAY_SDL2
#if DISPLAY_SHADER == 1
// Palette textures not supported, convert to GL_RED style texture
// so shader can perform the lookup.
uint8_t formatted[width * height];
for(int32_t i = 0; i < width * height; i++) {
uint8_t index = data.paletteData[i];
formatted[i] = index * 128;
}
glTexImage2D(
GL_TEXTURE_2D, 0, GL_R8, width, height, 0,
GL_RED, GL_UNSIGNED_BYTE, (void*)formatted
);
#else
glTexImage2D(
GL_TEXTURE_2D,
0, GL_COLOR_INDEX8_EXT,
width, height,
0, GL_COLOR_INDEX8_EXT,
GL_UNSIGNED_BYTE, (void*)data.paletteData
);
// glColorTableEXT(
// GL_TEXTURE_2D, GL_RGBA, data.palette.palette->colorCount, GL_RGBA,
// GL_UNSIGNED_BYTE, (const void*)data.palette.palette->colors
// );
#endif
#else
#error "Palette textures not supported on this platform"
#endif
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct {
} palettetexture_t;
/**
* Initializes a palette texture.
*
* @param texture The palette texture to initialize.
* @param width The width of the texture. Must be a power of 2.
* @param height The height of the texture. Must be a power of 2.
* @param data The palette index data for the texture.
*/
void paletteTextureInit(
palettetexture_t *texture,
const int32_t width,
const int32_t height,
const uint8_t *data
);

View File

@@ -52,12 +52,13 @@ void textureInit(
uint8_t formatted[width * height];
for(int32_t i = 0; i < width * height; i++) {
uint8_t index = data.paletteData[i];
formatted[i] = index;
formatted[i] = index * 128;
}
glTexImage2D(
GL_TEXTURE_2D, 0, GL_R, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, (void*)formatted
GL_TEXTURE_2D, 0, GL_R8, width, height, 0,
GL_RED, GL_UNSIGNED_BYTE, (void*)formatted
);
} else {
glTexImage2D(
GL_TEXTURE_2D,
@@ -66,12 +67,17 @@ void textureInit(
0, GL_COLOR_INDEX8_EXT,
GL_UNSIGNED_BYTE, (void*)data.paletteData
);
// glColorTableEXT(
// GL_TEXTURE_2D, GL_RGBA, data.palette.palette->colorCount, GL_RGBA,
// GL_UNSIGNED_BYTE, (const void*)data.palette.palette->colors
// );
}
GLenum err = glGetError();
if(err != GL_NO_ERROR) {
printf("GL Error uploading palette texture: %d\n", err);
assertUnreachable("GL error uploading palette texture");
}
break;
default:

View File

@@ -8,7 +8,7 @@
#pragma once
#include "display/color.h"
#include "display/displaydefs.h"
#include "display/palette/palette.h"
#include "display/texture/palette.h"
typedef enum {
#if DISPLAY_SDL2

View File

@@ -9,14 +9,12 @@
#include "dusk.h"
typedef struct tileset_s {
const char_t *name;
const uint16_t tileWidth;
const uint16_t tileHeight;
const uint16_t tileCount;
const uint16_t columns;
const uint16_t rows;
const vec2 uv;
const char_t *image;
uint16_t tileWidth;
uint16_t tileHeight;
uint16_t tileCount;
uint16_t columns;
uint16_t rows;
vec2 uv;
} tileset_t;
/**

View File

@@ -1,10 +0,0 @@
# Copyright (c) 2025 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
tileset.c
)

View File

@@ -33,11 +33,6 @@
#include <ogcsys.h>
#include <gccore.h>
#include <malloc.h>
#include <sys/endian.h>
#else
#ifndef le32toh
#define le32toh(x) (x)
#endif
#endif
typedef bool bool_t;

View File

@@ -11,30 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
inputaction.c
)
if(DUSK_TARGET_SYSTEM STREQUAL "linux")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
INPUT_SDL2=1
INPUT_KEYBOARD=1
INPUT_POINTER=1
INPUT_GAMEPAD=1
)
elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
INPUT_SDL2=1
INPUT_GAMEPAD=1
)
elseif(DUSK_TARGET_SYSTEM STREQUAL "gamecube" OR DUSK_TARGET_SYSTEM STREQUAL "wii")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
INPUT_GAMEPAD=1
)
endif()
# CSV
# Input Action Definitions
dusk_run_python(
dusk_input_csv_defs
tools.input.csv

View File

@@ -10,6 +10,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
backpack.c
)
# Item Definitions
dusk_run_python(
dusk_item_csv_defs
tools.item.csv

View File

@@ -7,7 +7,7 @@
#include "moduletexture.h"
#include "asset/asset.h"
#include "display/texture.h"
#include "display/texture/texture.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"

View File

@@ -7,7 +7,7 @@
#include "moduletileset.h"
#include "assert/assert.h"
#include "display/tileset/tileset.h"
#include "display/texture/tileset.h"
#include "util/memory.h"
#include "util/string.h"
#include "debug/debug.h"
@@ -41,13 +41,7 @@ int moduleTilesetIndex(lua_State *l) {
tileset_t *ts = (tileset_t *)luaL_checkudata(l, 1, "tileset_mt");
assertNotNull(ts, "Tileset pointer cannot be NULL.");
if(stringCompare(key, "name") == 0) {
lua_pushstring(l, ts->name);
return 1;
} else if(stringCompare(key, "texture") == 0) {
lua_pushstring(l, ts->image);
return 1;
} else if(stringCompare(key, "tileWidth") == 0) {
if(stringCompare(key, "tileWidth") == 0) {
lua_pushnumber(l, ts->tileWidth);
return 1;
} else if(stringCompare(key, "tileHeight") == 0) {
@@ -72,7 +66,9 @@ int moduleTilesetToString(lua_State *l) {
tileset_t *ts = (tileset_t *)luaL_checkudata(l, 1, "tileset_mt");
assertNotNull(ts, "Tileset pointer cannot be NULL.");
lua_pushfstring(l, "Tileset: %s", ts->name);
lua_pushfstring(l, "Tileset: %dx%d tile, %d columns, %d rows",
ts->tileWidth, ts->tileHeight, ts->columns, ts->rows
);
return 1;
}

View File

@@ -9,6 +9,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
storyflag.c
)
# Story Flag Definitions
dusk_run_python(
dusk_story_defs
tools.story.csv

View File

@@ -9,20 +9,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
thread.c
threadmutex.c
)
# Compiler flags.
if(DUSK_TARGET_SYSTEM STREQUAL "linux")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
THREAD_PTHREAD=1
)
elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
THREAD_PTHREAD=1
)
elseif(DUSK_TARGET_SYSTEM STREQUAL "gamecube" OR DUSK_TARGET_SYSTEM STREQUAL "wii")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
)
endif()

View File

@@ -8,22 +8,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
time.c
)
# Compiler defs
if(DUSK_TARGET_SYSTEM STREQUAL "linux")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
TIME_SDL2=1
TIME_FIXED=0
)
elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
TIME_FIXED=1
)
elseif(DUSK_TARGET_SYSTEM STREQUAL "gamecube" OR DUSK_TARGET_SYSTEM STREQUAL "wii")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
TIME_FIXED=1
)
endif()

View File

@@ -7,6 +7,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
array.c
endian.c
memory.c
string.c
math.c

67
src/util/endian.c Normal file
View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "util/endian.h"
#include "util/memory.h"
bool_t isHostLittleEndian(void) {
uint32_t value = ENDIAN_MAGIC;
uint8_t *bytePtr = (uint8_t *)&value;
return bytePtr[0] == 0x04;
}
uint32_t endianLittleToHost32(uint32_t value) {
if(isHostLittleEndian()) {
return value;
}
return (
((value & 0x000000FF) << 24) |
((value & 0x0000FF00) << 8) |
((value & 0x00FF0000) >> 8) |
((value & 0xFF000000) >> 24)
);
}
uint16_t endianLittleToHost16(uint16_t value) {
if(isHostLittleEndian()) {
return value;
}
return (uint16_t)(((value & 0x00FF) << 8) |
((value & 0xFF00) >> 8));
}
uint64_t endianLittleToHost64(uint64_t value) {
if(isHostLittleEndian()) {
return value;
}
return (
((value & 0x00000000000000FFULL) << 56) |
((value & 0x000000000000FF00ULL) << 40) |
((value & 0x0000000000FF0000ULL) << 24) |
((value & 0x00000000FF000000ULL) << 8) |
((value & 0x000000FF00000000ULL) >> 8) |
((value & 0x0000FF0000000000ULL) >> 24) |
((value & 0x00FF000000000000ULL) >> 40) |
((value & 0xFF00000000000000ULL) >> 56)
);
}
float_t endianLittleToHostFloat(float_t value) {
if(isHostLittleEndian()) {
return value;
}
uint32_t temp;
float_t result;
memoryCopy(&temp, &value, sizeof(uint32_t));
temp = endianLittleToHost32(temp);
memoryCopy(&result, &temp, sizeof(uint32_t));
return result;
}

48
src/util/endian.h Normal file
View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
static const uint32_t ENDIAN_MAGIC = 0x01020304;
/**
* Checks if the host system is little-endian.
*
* @return true if the host is little-endian, false otherwise.
*/
bool_t isHostLittleEndian(void);
/**
* Converts a 32-bit integer from little-endian to host byte order.
*
* @param value The little-endian value to convert.
* @return The value in host byte order.
*/
uint32_t endianLittleToHost32(uint32_t value);
/**
* Converts a 16-bit integer from little-endian to host byte order.
* @param value The little-endian value to convert.
* @return The value in host byte order.
*/
uint16_t endianLittleToHost16(uint16_t value);
/**
* Converts a 64-bit integer from little-endian to host byte order.
* @param value The little-endian value to convert.
* @return The value in host byte order.
*/
uint64_t endianLittleToHost64(uint64_t value);
/**
* Converts a float from little-endian to host byte order.
*
* @param value The little-endian value to convert.
* @return The value in host byte order.
*/
float_t endianLittleToHostFloat(float_t value);

View File

@@ -661,7 +661,7 @@
const header = new Uint8Array([0x44, 0x50, 0x54, 0x01]); // 'DPT' + version 1
// Dimensions
const widthBytes = new Uint8Array([ imageWidth ]);
const widthBytes = new Uint32Array([ imageWidth ]);
const heightBytes = new Uint32Array([ imageHeight ]);
// add indexed image data (imageWidth * imageHeight bytes)

View File

@@ -94,6 +94,9 @@
return onBadImages('Please select 2 or more image images.');
}
// Sort images by name to ensure consistent output
images = Object.fromEntries(Object.entries(images).sort(([nameA], [nameB]) => nameA.localeCompare(nameB)));
elFileError.style.display = 'none';
let strInfo = `Selected ${Object.keys(images).length} images:\n`;
@@ -119,7 +122,16 @@
outputHeight = firstHeight;
outputWidth = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.width, 0));
} else {
onBadImages('All images must share the same width or height, and that dimension must be a power of two.');
if(allImagesShareWidth) {
outputWidth = nextPowerOfTwo(firstWidth);
outputHeight = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.height, 0));
} else if(allImagesShareHeight) {
outputHeight = nextPowerOfTwo(firstHeight);
outputWidth = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.width, 0));
} else {
onBadImages('All images must share the same width or height to be joined together.');
return;
}
}
// Update preview

View File

@@ -30,6 +30,26 @@
<div>
<h2>Tileset Settings</h2>
<div>
<button data-load-tileset>Load Tileset</button>
</div>
<div>
Define tile count by:
<div>
<label>
Tile Size
<input type="radio" name="define-by" value="size" checked />
</label>
<label>
Tile Count
<input type="radio" name="define-by" value="count" />
</label>
</div>
</div>
<div data-tile-sizes>
<div>
<label>Tile Width:</label>
<input type="number" value="8" data-tile-width min="1" step="1" />
@@ -38,6 +58,8 @@
<label>Tile Height:</label>
<input type="number" value="8" data-tile-height min="1" step="1" />
</div>
</div>
<div data-tile-counts style="display: none;">
<div>
<label>Column Count:</label>
<input type="number" value="10" data-column-count min="1" step="1" />
@@ -48,6 +70,16 @@
</div>
</div>
<div>
<label>Unused Space on Right of Texture:</label>
<input type="number" value="0" data-right min="0" step="1" />
</div>
<div>
<label>Unused Space on Bottom of Texture:</label>
<input type="number" value="0" data-bottom min="0" step="1" />
</div>
</div>
<div>
<h2>Preview</h2>
<div>
@@ -80,9 +112,15 @@
<div>
<button data-tileset-download>Download Tileset</button>
</div>
</div>
</body>
<script type="text/javascript">
// Element selectors
const elDefineBySize = document.querySelector('input[name="define-by"][value="size"]');
const elDefineByCount = document.querySelector('input[name="define-by"][value="count"]');
const elTileSizes = document.querySelector('[data-tile-sizes]');
const elTileCounts = document.querySelector('[data-tile-counts]');
const elTileWidth = document.querySelector('[data-tile-width]');
const elTileHeight = document.querySelector('[data-tile-height]');
const elColumnCount = document.querySelector('[data-column-count]');
@@ -92,6 +130,8 @@
const elOutputInformation = document.querySelector('[data-output-information]');
const elOutputPreview = document.querySelector('[data-output-preview]');
const elScale = document.querySelector('[data-indexed-preview-scale]');
const elRight = document.querySelector('[data-right]');
const elBottom = document.querySelector('[data-bottom]');
const btnDownloadTileset = document.querySelector('[data-tileset-download]');
const btnBackgroundWhite = document.querySelector('[data-page-bg-white]');
const btnBackgroundTransparent = document.querySelector('[data-page-bg-transparent]');
@@ -99,130 +139,181 @@
const btnBackgroundMagenta = document.querySelector('[data-page-bg-magenta]');
const btnBackgroundBlue = document.querySelector('[data-page-bg-blue]');
const btnBackgroundGreen = document.querySelector('[data-page-bg-green]');
const btnDownloadPalette = document.querySelector('[data-palette-download]');
const btnDownloadImage = document.querySelector('[data-indexed-download]');
const btnLoadTileset = document.querySelector('[data-load-tileset]');
// State
let imageWidth = 0;
let imageHeight = 0;
let image = null;
let pixels = null;
let hoveredX = -1;
let hoveredY = -1;
const updatePreview = () => {
if(!image) return;
const getValues = () => {
if(!pixels) return null;
let tileWidth, tileHeight, columnCount, rowCount;
if(elDefineBySize.checked) {
console.log('Defining by size');
tileWidth = parseInt(elTileWidth.value) || 0;
tileHeight = parseInt(elTileHeight.value) || 0;
columnCount = Math.floor(imageWidth / tileWidth);
rowCount = Math.floor(imageHeight / tileHeight);
} else {
console.log('Defining by count');
columnCount = parseInt(elColumnCount.value) || 0;
rowCount = parseInt(elRowCount.value) || 0;
tileWidth = Math.floor(imageWidth / columnCount);
tileHeight = Math.floor(imageHeight / rowCount);
}
const right = parseInt(elRight.value) || 0;
const bottom = parseInt(elBottom.value) || 0;
const scale = parseInt(elScale.value) || 1;
elOutputPreview.width = imageWidth * scale;
elOutputPreview.height = imageHeight * scale;
const scaledWidth = imageWidth * scale;
const scaledHeight = imageHeight * scale;
const scaledTileWidth = tileWidth * scale;
const scaledTileHeight = tileHeight * scale;
const scaledRight = right * scale;
const scaledBottom = bottom * scale;
const u0 = (tileWidth / imageWidth);
const v0 = (tileHeight / imageHeight);
const hoveredTileX = isNaN(hoveredX) || hoveredX < 0 ? 0 : hoveredX;
const hoveredTileY = isNaN(hoveredY) || hoveredY < 0 ? 0 : hoveredY;
const hoveredU0 = hoveredTileX * u0;
const hoveredV0 = hoveredTileY * v0;
const hoveredU1 = hoveredU0 + u0;
const hoveredV1 = hoveredV0 + v0;
const hoveredTileIndex = hoveredTileY * columnCount + hoveredTileX;
return {
tileWidth,
tileHeight,
columnCount,
rowCount,
right,
bottom,
scale,
scaledWidth,
scaledHeight,
scaledTileWidth,
scaledTileHeight,
scaledRight,
scaledBottom,
u0,
v0,
hoveredU0,
hoveredV0,
hoveredU1,
hoveredV1,
hoveredTileX,
hoveredTileY,
hoveredTileIndex,
}
}
const updatePreview = () => {
const v = getValues();
if(!v) return;
// console.log('Updating preview with values', v);
// Prepare canvas
elOutputPreview.width = v.scaledWidth;
elOutputPreview.height = v.scaledHeight;
const ctx = elOutputPreview.getContext('2d');
ctx.clearRect(0, 0, elOutputPreview.width, elOutputPreview.height);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(image, 0, 0, elOutputPreview.width, elOutputPreview.height);
// Draw grid lines
const tileWidth = parseInt(elTileWidth.value) || 0;
const tileHeight = parseInt(elTileHeight.value) || 0;
const scaledTileWidth = tileWidth * scale;
const scaledTileHeight = tileHeight * scale;
const columnCount = parseInt(elColumnCount.value) || 0;
const rowCount = parseInt(elRowCount.value) || 0;
// Resize pixels
const tempCanvas = document.createElement('canvas');
tempCanvas.width = imageWidth;
tempCanvas.height = imageHeight;
const tempCtx = tempCanvas.getContext('2d');
const imageData = tempCtx.createImageData(imageWidth, imageHeight);
imageData.data.set(pixels);
tempCtx.putImageData(imageData, 0, 0);
ctx.drawImage(tempCanvas, 0, 0, elOutputPreview.width, elOutputPreview.height);
// Draw blue overflow area for right and bottom cutoff
ctx.fillStyle = 'rgba(0,0,255,0.5)';
if(v.right > 0) {
ctx.fillRect(v.scaledWidth - v.scaledRight, 0, v.scaledRight, elOutputPreview.height);
}
if(v.bottom > 0) {
ctx.fillRect(0, v.scaledHeight - v.scaledBottom, elOutputPreview.width, v.scaledBottom);
}
// Draw red grid lines for tile boundaries
ctx.strokeStyle = 'rgba(255,0,0,1)';
for(let x = scaledTileWidth; x < elOutputPreview.width; x += scaledTileWidth) {
for (let x = v.scaledTileWidth; x < elOutputPreview.width; x += v.scaledTileWidth) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, elOutputPreview.height);
ctx.stroke();
}
for(let y = scaledTileHeight; y < elOutputPreview.height; y += scaledTileHeight) {
for (let y = v.scaledTileHeight; y < elOutputPreview.height; y += v.scaledTileHeight) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(elOutputPreview.width, y);
ctx.stroke();
}
const u0 = tileWidth / imageWidth;
const v0 = tileHeight / imageHeight;
const hoveredU0 = hoveredX != -1 && hoveredY != -1 ? (hoveredX * tileWidth / imageWidth) : 0;
const hoveredV0 = hoveredX != -1 && hoveredY != -1 ? (hoveredY * tileHeight / imageHeight) : 0;
const hoveredU1 = hoveredU0 + u0;
const hoveredV1 = hoveredV0 + v0;
elOutputInformation.value = [
hoveredX != -1 ? `Hovered Tile: ${hoveredX}, ${hoveredY} (${hoveredY * columnCount + hoveredX})` : 'Hovered Tile: None',
hoveredX != -1 ? `Hovered UV: ${(hoveredU0).toFixed(4)}, ${(hoveredV0).toFixed(4)} -> ${(hoveredU1).toFixed(4)}, ${(hoveredV1).toFixed(4)}` : 'Hovered UV: None',
v.hoveredX != -1 ? `Hovered Tile: ${v.hoveredTileX}, ${v.hoveredTileY} (${v.hoveredTileIndex})` : 'Hovered Tile: None',
v.hoveredX != -1 ? `Hovered UV: ${(v.hoveredU0).toFixed(4)}, ${(v.hoveredV0).toFixed(4)} -> ${(v.hoveredU1).toFixed(4)}, ${(v.hoveredV1).toFixed(4)}` : 'Hovered UV: None',
`Image Width: ${imageWidth}`,
`Image Height: ${imageHeight}`,
`Tile Width: ${elTileWidth.value}`,
`Tile Height: ${elTileHeight.value}`,
`Column Count: ${columnCount}`,
`uv: ${u0.toFixed(4)}, ${v0.toFixed(4)}`,
`Row Count: ${rowCount}`,
`Tile count: ${columnCount * rowCount}`,
`Tile Width: ${v.tileWidth}`,
`Tile Height: ${v.tileHeight}`,
`Column Count: ${v.columnCount}`,
`uv: ${v.u0.toFixed(4)}, ${v.v0.toFixed(4)}`,
`Row Count: ${v.rowCount}`,
`Tile count: ${v.columnCount * v.rowCount}`,
].join('\n');
}
elTileWidth.addEventListener('input', () => {
if(imageWidth) {
const tileWidth = parseInt(elTileWidth.value);
const columnCount = Math.floor(imageWidth / tileWidth);
elColumnCount.value = columnCount;
elTileWidth.addEventListener('input', updatePreview);
elTileHeight.addEventListener('input', updatePreview);
elColumnCount.addEventListener('input', updatePreview);
elRowCount.addEventListener('input', updatePreview);
elRight.addEventListener('input', updatePreview);
elBottom.addEventListener('input', updatePreview);
elScale.addEventListener('input', updatePreview);
btnBackgroundWhite.addEventListener('click', () => document.body.style.background = 'white');
btnBackgroundTransparent.addEventListener('click', () => document.body.style.background = 'black');
btnBackgroundCheckerboard.addEventListener('click', () => document.body.style.background = 'repeating-conic-gradient(#ccc 0% 25%, #eee 0% 50%) 50% / 20px 20px');
btnBackgroundMagenta.addEventListener('click', () => document.body.style.background = 'magenta');
btnBackgroundBlue.addEventListener('click', () => document.body.style.background = 'blue');
btnBackgroundGreen.addEventListener('click', () => document.body.style.background = 'green');
elDefineBySize.addEventListener('change', () => {
if (elDefineBySize.checked) {
elTileSizes.style.display = '';
elTileCounts.style.display = 'none';
}
updatePreview();
});
elTileHeight.addEventListener('input', () => {
if(imageHeight) {
const tileHeight = parseInt(elTileHeight.value);
const rowCount = Math.floor(imageHeight / tileHeight);
elRowCount.value = rowCount;
elDefineByCount.addEventListener('change', () => {
if (elDefineByCount.checked) {
elTileSizes.style.display = 'none';
elTileCounts.style.display = '';
}
updatePreview();
});
elColumnCount.addEventListener('input', () => {
if(!imageWidth) {
alert('Set an image first to calculate tile width from column count');
return;
}
const columnCount = parseInt(elColumnCount.value);
const tileWidth = Math.floor(imageWidth / columnCount);
elTileWidth.value = tileWidth;
updatePreview();
});
elRowCount.addEventListener('input', () => {
if(!imageHeight) {
alert('Set an image first to calculate tile height from row count');
return;
}
const rowCount = parseInt(elRowCount.value);
const tileHeight = Math.floor(imageHeight / rowCount);
elTileHeight.value = tileHeight;
updatePreview();
});
elScale.addEventListener('input', () => {
updatePreview();
});
elOutputPreview.addEventListener('mousemove', (e) => {
if(!image) return;
const scale = parseInt(elScale.value) || 1;
const tileWidth = parseInt(elTileWidth.value) || 0;
const tileHeight = parseInt(elTileHeight.value) || 0;
const columnCount = parseInt(elColumnCount.value) || 0;
const rowCount = parseInt(elRowCount.value) || 0;
const values = getValues();
if(!values) return;
const rect = elOutputPreview.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / scale);
const y = Math.floor((e.clientY - rect.top) / scale);
hoveredX = Math.floor(x / tileWidth);
hoveredY = Math.floor(y / tileHeight);
if(hoveredX < 0 || hoveredX >= columnCount || hoveredY < 0 || hoveredY >= rowCount) {
const x = Math.floor((e.clientX - rect.left) / values.scale);
const y = Math.floor((e.clientY - rect.top) / values.scale);
hoveredX = Math.floor(x / values.tileWidth);
hoveredY = Math.floor(y / values.tileHeight);
if(hoveredX < 0 || hoveredX >= values.columnCount || hoveredY < 0 || hoveredY >= values.rowCount) {
hoveredX = -1;
hoveredY = -1;
}
@@ -239,65 +330,144 @@
// File
elFileInput.addEventListener('change', (e) => {
elOutputError.style.display = 'none';
pixels = null;
if(!elFileInput.files.length) {
if (!elFileInput.files.length) {
elOutputError.textContent = 'No file selected';
elOutputError.style.display = 'block';
return;
}
const file = elFileInput.files[0];
image = new Image();
if (file.name.endsWith('.dpt')) {
// Load DPT file
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const data = new Uint8Array(arrayBuffer);
if (data[0] !== 'D'.charCodeAt(0) || data[1] !== 'P'.charCodeAt(0) || data[2] !== 'T'.charCodeAt(0)) {
elOutputError.textContent = 'Invalid DPT file';
elOutputError.style.display = 'block';
return;
} else if (data[3] !== 0x01) {
elOutputError.textContent = 'Unsupported DPT version';
elOutputError.style.display = 'block';
return;
}
// Begin color indexes
const width = (
data[4] |
(data[5] << 8) |
(data[6] << 16) |
(data[7] << 24)
)
const height = (
data[8] |
(data[9] << 8) |
(data[10] << 16) |
(data[11] << 24)
);
imageWidth = width;
imageHeight = height;
if(data.length < 12 + width * height) {
elOutputError.textContent = 'Invalid DPT file: not enough pixel data';
elOutputError.style.display = 'block';
return;
}
const uniqueIndexes = [];
for (let i = 0; i < width * height; i++) {
const colorIndex = data[12 + i];
if (!uniqueIndexes.includes(colorIndex)) {
uniqueIndexes.push(colorIndex);
}
}
const adhocPalette = [];
for (let i = 0; i < uniqueIndexes.length; i++) {
const index = uniqueIndexes[i];
// Get the most different possible color for this index
const color = [
(index * 37) % 256,
(index * 61) % 256,
(index * 97) % 256,
255
];
adhocPalette[index] = color;
}
pixels = new Uint8Array(width * height * 4);
for (let i = 0; i < width * height; i++) {
const colorIndex = data[12 + i];
const color = adhocPalette[colorIndex];
pixels[i * 4] = color[0];
pixels[i * 4 + 1] = color[1];
pixels[i * 4 + 2] = color[2];
pixels[i * 4 + 3] = color[3];
}
updatePreview();
}
reader.onerror = () => {
elOutputError.textContent = 'Failed to read file';
elOutputError.style.display = 'block';
}
reader.readAsArrayBuffer(file);
} else {
const image = new Image();
image.onload = () => {
imageWidth = image.width;
imageHeight = image.height;
const tileWidth = parseInt(elTileWidth.value);
const tileHeight = parseInt(elTileHeight.value);
const columnCount = Math.floor(imageWidth / tileWidth);
const rowCount = Math.floor(imageHeight / tileHeight);
elColumnCount.value = columnCount;
elRowCount.value = rowCount;
// Pixels
const tempCanvas = document.createElement('canvas');
tempCanvas.width = imageWidth;
tempCanvas.height = imageHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(image, 0, 0);
const imageData = tempCtx.getImageData(0, 0, imageWidth, imageHeight);
pixels = imageData.data;
updatePreview();
};
image.onerror = () => {
image = null;
pixels = null;
elOutputError.textContent = 'Failed to load image';
elOutputError.style.display = 'block';
updatePreview();
};
image.src = URL.createObjectURL(file);
}
});
btnDownloadTileset.addEventListener('click', () => {
if(!image) {
alert('No image loaded');
const v = getValues();
if(!v) {
alert('No valid tileset to download');
return;
}
const tileWidth = parseInt(elTileWidth.value);
const tileHeight = parseInt(elTileHeight.value);
const columnCount = parseInt(elColumnCount.value);
const rowCount = parseInt(elRowCount.value);
const u0 = tileWidth / imageWidth;
const v0 = tileHeight / imageHeight;
const tileCount = columnCount * rowCount;
// Header: DTF0, tileWidth, tileHeight, columnCount, rowCount, right, bottom, u0, v0
const headerBytes = new Uint8Array([
'D'.charCodeAt(0),// Dusk
'T'.charCodeAt(0),// Tileset
'F'.charCodeAt(0),// File/Format
'D'.charCodeAt(0), // Dusk
'T'.charCodeAt(0), // Tileset
'F'.charCodeAt(0), // File/Format
0x00, // version
tileWidth & 0xFF,// Tile width (uint16_t)
(tileWidth >> 8) & 0xFF,
tileHeight & 0xFF,// Tile height (uint16_t)
(tileHeight >> 8) & 0xFF,
columnCount & 0xFF,// Column count (uint16_t)
(columnCount >> 8) & 0xFF,
rowCount & 0xFF,// Row count (uint16_t)
(rowCount >> 8) & 0xFF,
// Float32_t UV step (u0, v0)
...new Uint8Array(new Float32Array([u0]).buffer),
...new Uint8Array(new Float32Array([v0]).buffer),
v.tileWidth & 0xFF, (v.tileWidth >> 8) & 0xFF,
v.tileHeight & 0xFF, (v.tileHeight >> 8) & 0xFF,
v.columnCount & 0xFF, (v.columnCount >> 8) & 0xFF,
v.rowCount & 0xFF, (v.rowCount >> 8) & 0xFF,
v.right & 0xFF, (v.right >> 8) & 0xFF,
v.bottom & 0xFF, (v.bottom >> 8) & 0xFF,
...new Uint8Array(new Float32Array([v.u0]).buffer),
...new Uint8Array(new Float32Array([v.v0]).buffer),
]);
// Download file
@@ -307,25 +477,65 @@
a.href = url;
a.download = 'tileset.dtf';
a.click();
URL.revokeObjectURL(url);
});
btnBackgroundWhite.addEventListener('click', () => {
document.body.style.background = 'white';
btnLoadTileset.addEventListener('click', () => {
// Browse for file.
const input = document.createElement('input');
input.type = 'file';
input.accept = '.dtf';
input.addEventListener('change', (e) => {
const files = e?.target?.files;
if (!files || !files.length || !files[0]) {
alert('No file selected');
return;
}
const file = files[0];
if (!file.name.endsWith('.dtf')) {
alert('Invalid file type. Please select a .dtf file.');
return;
}
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const data = new Uint8Array(arrayBuffer);
if (data[0] !== 'D'.charCodeAt(0) || data[1] !== 'T'.charCodeAt(0) || data[2] !== 'F'.charCodeAt(0)) {
alert('Invalid DTF file');
return;
}
if (data[3] !== 0x00) {
alert('Unsupported DTF version');
return;
}
const tileWidth = data[4] | (data[5] << 8);
const tileHeight = data[6] | (data[7] << 8);
const columnCount = data[8] | (data[9] << 8);
const rowCount = data[10] | (data[11] << 8);
const right = data[12] | (data[13] << 8);
const bottom = data[14] | (data[15] << 8);
// Switch to using size definition
elDefineBySize.checked = true;
elTileWidth.value = tileWidth;
elTileHeight.value = tileHeight;
elTileSizes.style.display = '';
elTileCounts.style.display = 'none';
elRight.value = right;
elBottom.value = bottom;
updatePreview();
};
reader.onerror = () => {
alert('Failed to read file');
};
reader.readAsArrayBuffer(file);
});
btnBackgroundTransparent.addEventListener('click', () => {
document.body.style.background = 'black';
});
btnBackgroundCheckerboard.addEventListener('click', () => {
document.body.style.background = 'repeating-conic-gradient(#ccc 0% 25%, #eee 0% 50%) 50% / 20px 20px';
});
btnBackgroundMagenta.addEventListener('click', () => {
document.body.style.background = 'magenta';
});
btnBackgroundBlue.addEventListener('click', () => {
document.body.style.background = 'blue';
});
btnBackgroundGreen.addEventListener('click', () => {
document.body.style.background = 'green';
input.click();
});
// Init