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 - main
jobs: jobs:
run-tests: run-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

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

View File

@@ -187,19 +187,20 @@ function sceneRender()
if INPUT_POINTER then if INPUT_POINTER then
mouseX = inputGetValue(INPUT_ACTION_POINTERX) * screenGetWidth() mouseX = inputGetValue(INPUT_ACTION_POINTERX) * screenGetWidth()
mouseY = inputGetValue(INPUT_ACTION_POINTERY) * screenGetHeight() mouseY = inputGetValue(INPUT_ACTION_POINTERY) * screenGetHeight()
-- Draw cursor
spriteBatchPush(
nil,
mouseX - 2, mouseY - 2,
mouseX + 2, mouseY + 2,
colorRed(),
0, 0,
1, 1
)
end end
-- Draw cursor
spriteBatchPush(
nil,
mouseX - 2, mouseY - 2,
mouseX + 2, mouseY + 2,
colorRed(),
0, 0,
1, 1
)
textDraw(10, 10, "Minesweeper") textDraw(10, 10, "Hello World")
-- centerX = math.floor(screenGetWidth() / 2) -- centerX = math.floor(screenGetWidth() / 2)
-- centerY = math.floor(screenGetHeight() / 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 "asset/asset.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "map/mapchunk.h" #include "map/mapchunk.h"
#include "util/endian.h"
#pragma pack(push, 1) #pragma pack(push, 1)
typedef struct { typedef struct {
@@ -56,7 +57,7 @@ errorret_t assetMapChunkHandler(assetcustom_t custom) {
} }
// Fix endianess if necessary // Fix endianess if necessary
header.tileCount = le32toh(header.tileCount); header.tileCount = endianLittleToHost32(header.tileCount);
if(header.tileCount != CHUNK_TILE_COUNT) { if(header.tileCount != CHUNK_TILE_COUNT) {
zip_fclose(custom.zipFile); zip_fclose(custom.zipFile);
@@ -111,7 +112,7 @@ errorret_t assetMapChunkHandler(assetcustom_t custom) {
} }
// Fix endianess if necessary // Fix endianess if necessary
modelHeader.vertexCount = le32toh(modelHeader.vertexCount); modelHeader.vertexCount = endianLittleToHost32(modelHeader.vertexCount);
if( if(
vertexIndex + modelHeader.vertexCount > vertexIndex + modelHeader.vertexCount >

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,9 @@
#include "asset/asset.h" #include "asset/asset.h"
#include "assert/assert.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) { errorret_t assetTilesetLoad(assetentire_t entire) {
assertNotNull(entire.data, "Asset data cannot be null"); assertNotNull(entire.data, "Asset data cannot be null");
@@ -28,5 +30,41 @@ errorret_t assetTilesetLoad(assetentire_t entire) {
errorThrow("Unsupported tileset version"); 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 { typedef struct {
char_t header[3]; char_t header[3];
uint8_t version; 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; } assettileset_t;
#pragma pack(pop) #pragma pack(pop)

View File

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

View File

@@ -9,7 +9,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
display.c display.c
framebuffer.c framebuffer.c
screen.c screen.c
texture.c
spritebatch.c spritebatch.c
text.c text.c
) )
@@ -17,38 +16,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Subdirectories # Subdirectories
add_subdirectory(camera) add_subdirectory(camera)
add_subdirectory(mesh) add_subdirectory(mesh)
add_subdirectory(palette) add_subdirectory(texture)
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()
# Color definitions
dusk_run_python( dusk_run_python(
dusk_color_defs dusk_color_defs
tools.display.color.csv tools.display.color.csv

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,18 @@
#pragma once #pragma once
#include "display/color.h" #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_COUNT_MAX 4
#define PALETTE_COLOR_COUNT_MAX 0xFF #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]; uint8_t formatted[width * height];
for(int32_t i = 0; i < width * height; i++) { for(int32_t i = 0; i < width * height; i++) {
uint8_t index = data.paletteData[i]; uint8_t index = data.paletteData[i];
formatted[i] = index; formatted[i] = index * 128;
} }
glTexImage2D( glTexImage2D(
GL_TEXTURE_2D, 0, GL_R, width, height, 0, GL_TEXTURE_2D, 0, GL_R8, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, (void*)formatted GL_RED, GL_UNSIGNED_BYTE, (void*)formatted
); );
} else { } else {
glTexImage2D( glTexImage2D(
GL_TEXTURE_2D, GL_TEXTURE_2D,
@@ -66,12 +67,17 @@ void textureInit(
0, GL_COLOR_INDEX8_EXT, 0, GL_COLOR_INDEX8_EXT,
GL_UNSIGNED_BYTE, (void*)data.paletteData GL_UNSIGNED_BYTE, (void*)data.paletteData
); );
// glColorTableEXT( // glColorTableEXT(
// GL_TEXTURE_2D, GL_RGBA, data.palette.palette->colorCount, GL_RGBA, // GL_TEXTURE_2D, GL_RGBA, data.palette.palette->colorCount, GL_RGBA,
// GL_UNSIGNED_BYTE, (const void*)data.palette.palette->colors // 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; break;
default: default:

View File

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

View File

@@ -9,14 +9,12 @@
#include "dusk.h" #include "dusk.h"
typedef struct tileset_s { typedef struct tileset_s {
const char_t *name; uint16_t tileWidth;
const uint16_t tileWidth; uint16_t tileHeight;
const uint16_t tileHeight; uint16_t tileCount;
const uint16_t tileCount; uint16_t columns;
const uint16_t columns; uint16_t rows;
const uint16_t rows; vec2 uv;
const vec2 uv;
const char_t *image;
} tileset_t; } 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 <ogcsys.h>
#include <gccore.h> #include <gccore.h>
#include <malloc.h> #include <malloc.h>
#include <sys/endian.h>
#else
#ifndef le32toh
#define le32toh(x) (x)
#endif
#endif #endif
typedef bool bool_t; typedef bool bool_t;

View File

@@ -11,30 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
inputaction.c inputaction.c
) )
if(DUSK_TARGET_SYSTEM STREQUAL "linux") # Input Action Definitions
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
dusk_run_python( dusk_run_python(
dusk_input_csv_defs dusk_input_csv_defs
tools.input.csv tools.input.csv

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
#include "moduletileset.h" #include "moduletileset.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "display/tileset/tileset.h" #include "display/texture/tileset.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
#include "debug/debug.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"); tileset_t *ts = (tileset_t *)luaL_checkudata(l, 1, "tileset_mt");
assertNotNull(ts, "Tileset pointer cannot be NULL."); assertNotNull(ts, "Tileset pointer cannot be NULL.");
if(stringCompare(key, "name") == 0) { if(stringCompare(key, "tileWidth") == 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) {
lua_pushnumber(l, ts->tileWidth); lua_pushnumber(l, ts->tileWidth);
return 1; return 1;
} else if(stringCompare(key, "tileHeight") == 0) { } 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"); tileset_t *ts = (tileset_t *)luaL_checkudata(l, 1, "tileset_mt");
assertNotNull(ts, "Tileset pointer cannot be NULL."); 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; return 1;
} }

View File

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

View File

@@ -9,20 +9,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
thread.c thread.c
threadmutex.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 PUBLIC
time.c 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} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
array.c array.c
endian.c
memory.c memory.c
string.c string.c
math.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 const header = new Uint8Array([0x44, 0x50, 0x54, 0x01]); // 'DPT' + version 1
// Dimensions // Dimensions
const widthBytes = new Uint8Array([ imageWidth ]); const widthBytes = new Uint32Array([ imageWidth ]);
const heightBytes = new Uint32Array([ imageHeight ]); const heightBytes = new Uint32Array([ imageHeight ]);
// add indexed image data (imageWidth * imageHeight bytes) // add indexed image data (imageWidth * imageHeight bytes)

View File

@@ -94,6 +94,9 @@
return onBadImages('Please select 2 or more image images.'); 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'; elFileError.style.display = 'none';
let strInfo = `Selected ${Object.keys(images).length} images:\n`; let strInfo = `Selected ${Object.keys(images).length} images:\n`;
@@ -119,7 +122,16 @@
outputHeight = firstHeight; outputHeight = firstHeight;
outputWidth = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.width, 0)); outputWidth = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.width, 0));
} else { } 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 // Update preview

View File

@@ -31,20 +31,52 @@
<div> <div>
<h2>Tileset Settings</h2> <h2>Tileset Settings</h2>
<div> <div>
<label>Tile Width:</label> <button data-load-tileset>Load Tileset</button>
<input type="number" value="8" data-tile-width min="1" step="1" /> </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" />
</div>
<div>
<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" />
</div>
<div>
<label>Row Count:</label>
<input type="number" value="10" data-row-count min="1" step="1" />
</div>
</div>
<div>
<label>Unused Space on Right of Texture:</label>
<input type="number" value="0" data-right min="0" step="1" />
</div> </div>
<div> <div>
<label>Tile Height:</label> <label>Unused Space on Bottom of Texture:</label>
<input type="number" value="8" data-tile-height min="1" step="1" /> <input type="number" value="0" data-bottom min="0" step="1" />
</div>
<div>
<label>Column Count:</label>
<input type="number" value="10" data-column-count min="1" step="1" />
</div>
<div>
<label>Row Count:</label>
<input type="number" value="10" data-row-count min="1" step="1" />
</div> </div>
</div> </div>
@@ -61,28 +93,34 @@
</label> </label>
</div> </div>
<div> <div>
<label> <label>
Preview Background: Preview Background:
<button data-page-bg-white>White</button> <button data-page-bg-white>White</button>
<button data-page-bg-transparent>Black</button> <button data-page-bg-transparent>Black</button>
<button data-page-bg-checkerboard>Checkerboard</button> <button data-page-bg-checkerboard>Checkerboard</button>
<button data-page-bg-magenta>Magenta</button> <button data-page-bg-magenta>Magenta</button>
<button data-page-bg-blue>Blue</button> <button data-page-bg-blue>Blue</button>
<button data-page-bg-green>Green</button> <button data-page-bg-green>Green</button>
</label> </label>
</div> </div>
<div> <div>
<canvas data-output-preview style="border:1px solid black;"></canvas> <canvas data-output-preview style="border:1px solid black;"></canvas>
</div> </div>
<div> <div>
<textarea data-output-information rows="15" style="width: 500px;"></textarea> <textarea data-output-information rows="15" style="width: 500px;"></textarea>
</div> </div>
<div> <div>
<button data-tileset-download>Download Tileset</button> <button data-tileset-download>Download Tileset</button>
</div>
</div> </div>
</body> </body>
<script type="text/javascript"> <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 elTileWidth = document.querySelector('[data-tile-width]');
const elTileHeight = document.querySelector('[data-tile-height]'); const elTileHeight = document.querySelector('[data-tile-height]');
const elColumnCount = document.querySelector('[data-column-count]'); const elColumnCount = document.querySelector('[data-column-count]');
@@ -92,6 +130,8 @@
const elOutputInformation = document.querySelector('[data-output-information]'); const elOutputInformation = document.querySelector('[data-output-information]');
const elOutputPreview = document.querySelector('[data-output-preview]'); const elOutputPreview = document.querySelector('[data-output-preview]');
const elScale = document.querySelector('[data-indexed-preview-scale]'); 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 btnDownloadTileset = document.querySelector('[data-tileset-download]');
const btnBackgroundWhite = document.querySelector('[data-page-bg-white]'); const btnBackgroundWhite = document.querySelector('[data-page-bg-white]');
const btnBackgroundTransparent = document.querySelector('[data-page-bg-transparent]'); const btnBackgroundTransparent = document.querySelector('[data-page-bg-transparent]');
@@ -99,130 +139,181 @@
const btnBackgroundMagenta = document.querySelector('[data-page-bg-magenta]'); const btnBackgroundMagenta = document.querySelector('[data-page-bg-magenta]');
const btnBackgroundBlue = document.querySelector('[data-page-bg-blue]'); const btnBackgroundBlue = document.querySelector('[data-page-bg-blue]');
const btnBackgroundGreen = document.querySelector('[data-page-bg-green]'); const btnBackgroundGreen = document.querySelector('[data-page-bg-green]');
const btnDownloadPalette = document.querySelector('[data-palette-download]'); const btnLoadTileset = document.querySelector('[data-load-tileset]');
const btnDownloadImage = document.querySelector('[data-indexed-download]');
// State
let imageWidth = 0; let imageWidth = 0;
let imageHeight = 0; let imageHeight = 0;
let image = null; let pixels = null;
let hoveredX = -1; let hoveredX = -1;
let hoveredY = -1; let hoveredY = -1;
const updatePreview = () => { const getValues = () => {
if(!image) return; 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; const scale = parseInt(elScale.value) || 1;
elOutputPreview.width = imageWidth * scale; const scaledWidth = imageWidth * scale;
elOutputPreview.height = imageHeight * 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'); const ctx = elOutputPreview.getContext('2d');
ctx.clearRect(0, 0, elOutputPreview.width, elOutputPreview.height); ctx.clearRect(0, 0, elOutputPreview.width, elOutputPreview.height);
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
ctx.drawImage(image, 0, 0, elOutputPreview.width, elOutputPreview.height);
// Draw grid lines // Resize pixels
const tileWidth = parseInt(elTileWidth.value) || 0; const tempCanvas = document.createElement('canvas');
const tileHeight = parseInt(elTileHeight.value) || 0; tempCanvas.width = imageWidth;
const scaledTileWidth = tileWidth * scale; tempCanvas.height = imageHeight;
const scaledTileHeight = tileHeight * scale; const tempCtx = tempCanvas.getContext('2d');
const columnCount = parseInt(elColumnCount.value) || 0; const imageData = tempCtx.createImageData(imageWidth, imageHeight);
const rowCount = parseInt(elRowCount.value) || 0; 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)'; 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.beginPath();
ctx.moveTo(x, 0); ctx.moveTo(x, 0);
ctx.lineTo(x, elOutputPreview.height); ctx.lineTo(x, elOutputPreview.height);
ctx.stroke(); 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.beginPath();
ctx.moveTo(0, y); ctx.moveTo(0, y);
ctx.lineTo(elOutputPreview.width, y); ctx.lineTo(elOutputPreview.width, y);
ctx.stroke(); 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 = [ elOutputInformation.value = [
hoveredX != -1 ? `Hovered Tile: ${hoveredX}, ${hoveredY} (${hoveredY * columnCount + hoveredX})` : 'Hovered Tile: None', v.hoveredX != -1 ? `Hovered Tile: ${v.hoveredTileX}, ${v.hoveredTileY} (${v.hoveredTileIndex})` : '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 UV: ${(v.hoveredU0).toFixed(4)}, ${(v.hoveredV0).toFixed(4)} -> ${(v.hoveredU1).toFixed(4)}, ${(v.hoveredV1).toFixed(4)}` : 'Hovered UV: None',
`Image Width: ${imageWidth}`, `Image Width: ${imageWidth}`,
`Image Height: ${imageHeight}`, `Image Height: ${imageHeight}`,
`Tile Width: ${elTileWidth.value}`, `Tile Width: ${v.tileWidth}`,
`Tile Height: ${elTileHeight.value}`, `Tile Height: ${v.tileHeight}`,
`Column Count: ${columnCount}`, `Column Count: ${v.columnCount}`,
`uv: ${u0.toFixed(4)}, ${v0.toFixed(4)}`, `uv: ${v.u0.toFixed(4)}, ${v.v0.toFixed(4)}`,
`Row Count: ${rowCount}`, `Row Count: ${v.rowCount}`,
`Tile count: ${columnCount * rowCount}`, `Tile count: ${v.columnCount * v.rowCount}`,
].join('\n'); ].join('\n');
} }
elTileWidth.addEventListener('input', () => { elTileWidth.addEventListener('input', updatePreview);
if(imageWidth) { elTileHeight.addEventListener('input', updatePreview);
const tileWidth = parseInt(elTileWidth.value); elColumnCount.addEventListener('input', updatePreview);
const columnCount = Math.floor(imageWidth / tileWidth); elRowCount.addEventListener('input', updatePreview);
elColumnCount.value = columnCount; 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(); updatePreview();
}); });
elTileHeight.addEventListener('input', () => { elDefineByCount.addEventListener('change', () => {
if(imageHeight) { if (elDefineByCount.checked) {
const tileHeight = parseInt(elTileHeight.value); elTileSizes.style.display = 'none';
const rowCount = Math.floor(imageHeight / tileHeight); elTileCounts.style.display = '';
elRowCount.value = rowCount;
} }
updatePreview(); 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) => { elOutputPreview.addEventListener('mousemove', (e) => {
if(!image) return; const values = getValues();
if(!values) 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 rect = elOutputPreview.getBoundingClientRect(); const rect = elOutputPreview.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / scale); const x = Math.floor((e.clientX - rect.left) / values.scale);
const y = Math.floor((e.clientY - rect.top) / scale); const y = Math.floor((e.clientY - rect.top) / values.scale);
hoveredX = Math.floor(x / tileWidth); hoveredX = Math.floor(x / values.tileWidth);
hoveredY = Math.floor(y / tileHeight); hoveredY = Math.floor(y / values.tileHeight);
if(hoveredX < 0 || hoveredX >= values.columnCount || hoveredY < 0 || hoveredY >= values.rowCount) {
if(hoveredX < 0 || hoveredX >= columnCount || hoveredY < 0 || hoveredY >= rowCount) {
hoveredX = -1; hoveredX = -1;
hoveredY = -1; hoveredY = -1;
} }
@@ -239,65 +330,144 @@
// File // File
elFileInput.addEventListener('change', (e) => { elFileInput.addEventListener('change', (e) => {
elOutputError.style.display = 'none'; elOutputError.style.display = 'none';
pixels = null;
if(!elFileInput.files.length) { if (!elFileInput.files.length) {
elOutputError.textContent = 'No file selected'; elOutputError.textContent = 'No file selected';
elOutputError.style.display = 'block'; elOutputError.style.display = 'block';
return; return;
} }
const file = elFileInput.files[0]; const file = elFileInput.files[0];
image = new Image();
image.onload = () => { if (file.name.endsWith('.dpt')) {
imageWidth = image.width; // Load DPT file
imageHeight = image.height; const reader = new FileReader();
const tileWidth = parseInt(elTileWidth.value);
const tileHeight = parseInt(elTileHeight.value); reader.onload = () => {
const columnCount = Math.floor(imageWidth / tileWidth); const arrayBuffer = reader.result;
const rowCount = Math.floor(imageHeight / tileHeight); const data = new Uint8Array(arrayBuffer);
elColumnCount.value = columnCount; if (data[0] !== 'D'.charCodeAt(0) || data[1] !== 'P'.charCodeAt(0) || data[2] !== 'T'.charCodeAt(0)) {
elRowCount.value = rowCount; elOutputError.textContent = 'Invalid DPT file';
updatePreview(); elOutputError.style.display = 'block';
}; return;
image.onerror = () => { } else if (data[3] !== 0x01) {
image = null; elOutputError.textContent = 'Unsupported DPT version';
elOutputError.textContent = 'Failed to load image'; elOutputError.style.display = 'block';
elOutputError.style.display = 'block'; return;
updatePreview(); }
};
image.src = URL.createObjectURL(file); // 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;
// 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 = () => {
pixels = null;
elOutputError.textContent = 'Failed to load image';
elOutputError.style.display = 'block';
updatePreview();
};
image.src = URL.createObjectURL(file);
}
}); });
btnDownloadTileset.addEventListener('click', () => { btnDownloadTileset.addEventListener('click', () => {
if(!image) { const v = getValues();
alert('No image loaded'); if(!v) {
alert('No valid tileset to download');
return; return;
} }
const tileWidth = parseInt(elTileWidth.value); // Header: DTF0, tileWidth, tileHeight, columnCount, rowCount, right, bottom, u0, v0
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;
const headerBytes = new Uint8Array([ const headerBytes = new Uint8Array([
'D'.charCodeAt(0),// Dusk 'D'.charCodeAt(0), // Dusk
'T'.charCodeAt(0),// Tileset 'T'.charCodeAt(0), // Tileset
'F'.charCodeAt(0),// File/Format 'F'.charCodeAt(0), // File/Format
0x00, // version 0x00, // version
tileWidth & 0xFF,// Tile width (uint16_t) v.tileWidth & 0xFF, (v.tileWidth >> 8) & 0xFF,
(tileWidth >> 8) & 0xFF, v.tileHeight & 0xFF, (v.tileHeight >> 8) & 0xFF,
tileHeight & 0xFF,// Tile height (uint16_t) v.columnCount & 0xFF, (v.columnCount >> 8) & 0xFF,
(tileHeight >> 8) & 0xFF, v.rowCount & 0xFF, (v.rowCount >> 8) & 0xFF,
columnCount & 0xFF,// Column count (uint16_t) v.right & 0xFF, (v.right >> 8) & 0xFF,
(columnCount >> 8) & 0xFF, v.bottom & 0xFF, (v.bottom >> 8) & 0xFF,
rowCount & 0xFF,// Row count (uint16_t) ...new Uint8Array(new Float32Array([v.u0]).buffer),
(rowCount >> 8) & 0xFF, ...new Uint8Array(new Float32Array([v.v0]).buffer),
// Float32_t UV step (u0, v0)
...new Uint8Array(new Float32Array([u0]).buffer),
...new Uint8Array(new Float32Array([v0]).buffer),
]); ]);
// Download file // Download file
@@ -307,25 +477,65 @@
a.href = url; a.href = url;
a.download = 'tileset.dtf'; a.download = 'tileset.dtf';
a.click(); a.click();
URL.revokeObjectURL(url);
}); });
btnBackgroundWhite.addEventListener('click', () => { btnLoadTileset.addEventListener('click', () => {
document.body.style.background = 'white'; // Browse for file.
}); const input = document.createElement('input');
btnBackgroundTransparent.addEventListener('click', () => { input.type = 'file';
document.body.style.background = 'black'; input.accept = '.dtf';
}); input.addEventListener('change', (e) => {
btnBackgroundCheckerboard.addEventListener('click', () => { const files = e?.target?.files;
document.body.style.background = 'repeating-conic-gradient(#ccc 0% 25%, #eee 0% 50%) 50% / 20px 20px'; if (!files || !files.length || !files[0]) {
}); alert('No file selected');
btnBackgroundMagenta.addEventListener('click', () => { return;
document.body.style.background = 'magenta'; }
});
btnBackgroundBlue.addEventListener('click', () => { const file = files[0];
document.body.style.background = 'blue'; if (!file.name.endsWith('.dtf')) {
}); alert('Invalid file type. Please select a .dtf file.');
btnBackgroundGreen.addEventListener('click', () => { return;
document.body.style.background = 'green'; }
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);
});
input.click();
}); });
// Init // Init