diff --git a/.gitignore b/.gitignore index 2c4f84b..be51d83 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ /*.g3a # Python bytecode - __pycache__/ +__pycache__/ # Common IDE files *.sublime-project diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f08c52..a36b832 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,24 +1,26 @@ cmake_minimum_required(VERSION 3.15) -project(Nooncraft) +project(Nooncraft LANGUAGES CXX) include(GenerateG3A) include(Fxconv) find_package(Gint 2.8 REQUIRED) set(SOURCES - src/main.c - # ... -) -# Shared assets, fx-9860G-only assets and fx-CG-50-only assets + src/block.cpp + src/graphics.cpp + src/main.cpp + src/render.cpp + src/world.cpp) set(ASSETS - # ... -) + assets-cg/font_nooncraft.png + assets-cg/blockinfo.yaml) fxconv_declare_assets(${ASSETS} WITH_METADATA) +fxconv_declare_converters(converters.py) add_executable(addin ${SOURCES} ${ASSETS}) -target_compile_options(addin PRIVATE -Wall -Wextra -Os) -target_link_libraries(addin Gint::Gint) +target_compile_options(addin PRIVATE -Wall -Wextra -Wno-narrowing -Os -std=c++20) +target_link_libraries(addin Gint::Gint -lsupc++) if("${FXSDK_PLATFORM_LONG}" STREQUAL fx9860G) message(FATAL_ERROR "This game is not supported on fx-9860G!") diff --git a/assets-cg/blockinfo.yaml b/assets-cg/blockinfo.yaml new file mode 100644 index 0000000..c3b486f --- /dev/null +++ b/assets-cg/blockinfo.yaml @@ -0,0 +1,12 @@ +- name: "AIR" + cluster: " " + walkable: yes + breakability: Fluid + +- name: "STONE" + cluster: "□□□□" + toolKind: Pickaxe + +- name: "COBBLESTONE" + cluster: "####" + toolKind: Pickaxe diff --git a/assets-cg/font_nooncraft.png b/assets-cg/font_nooncraft.png new file mode 100644 index 0000000..52b3d4a Binary files /dev/null and b/assets-cg/font_nooncraft.png differ diff --git a/assets-cg/fxconv-metadata.txt b/assets-cg/fxconv-metadata.txt index e69de29..1a019e2 100644 --- a/assets-cg/fxconv-metadata.txt +++ b/assets-cg/fxconv-metadata.txt @@ -0,0 +1,10 @@ +font_nooncraft.png: + type: font + name: font_nooncraft + charset: ascii + grid.size: 5x5 + grid.padding: 1 + +blockinfo.yaml: + custom-type: blockinfo + name: Nooncraft_blockInfo diff --git a/assets-cg/world.xcf b/assets-cg/world.xcf new file mode 100644 index 0000000..c92009c Binary files /dev/null and b/assets-cg/world.xcf differ diff --git a/converters.py b/converters.py new file mode 100644 index 0000000..0274ac3 --- /dev/null +++ b/converters.py @@ -0,0 +1,92 @@ +import fxconv +import yaml + +def convert(input, output, params, target): + recognized = True + if params["custom-type"] == "blockinfo": + o = convert_blockinfo(input, params) + else: + recognized = False + + if recognized: + fxconv.elf(o, output, "_" + params["name"], **target) + return 0 + + return 1 + +def mkBreakability(br): + return fxconv.u8({ + None: 0, + "Fluid": 0, + "ByAnything": 1, + "ByTool": 2, + "Unbreakable": 3, + }[br]) + +def mkToolKind(tk): + return fxconv.u8({ + None: 0, + "Pickaxe": 0, + "Axe": 1, + "Shovel": 2, + "Hoe": 3, + }[tk]) + +def mkBool(b): + if isinstance(b, str): + b = b.lower() + return fxconv.u8(b in [True, "yes", "true"]) + +def mkGlyph(s): + assert len(s) == 1 + special = { + 0x2058: 1, + 0x00B7: 2, + 0x2225: 3, + 0x00A4: 4, + 0x2197: 5, + 0x00BB: 6, + 0x2193: 7, + 0x00EE: 8, + 0x222B: 9, + 0x25AA: 10, + 0x2605: 11, + 0x25A1: 0x7f, + } + if ord(s) >= 0x20 and ord(s) <= 0x7f: + return fxconv.u8(ord(s)) + elif ord(s) in special: + return fxconv.u8(special[ord(s)]) + else: + raise ValueError(f"unknown glyph {s}") + +def mkGlyphCluster(str): + assert len(str) == 4 + return b"".join(mkGlyph(s) for s in str) + +def convert_blockinfo(input, params): + with open(input, "r") as fp: + data = yaml.safe_load(fp.read()) + + o = fxconv.ObjectData() + + for b in data: + # Make sure there are no typos in the fields + assert (f in ["name", "breakability", "toolKind", + "baseBreakDurationTicks", "walkable"] for f in b) + + # If a tool is specified, default to Breakability::ByTool + tk = b.get("toolKind", None) + br = b.get("breakability", "ByTool" if tk is not None else "Fluid") + + o += fxconv.string(b["name"]) + o += mkGlyphCluster(b["cluster"]) + o += mkBreakability(br) + o += mkToolKind(tk) + o += fxconv.u16(int(b.get("baseBreakDurationTicks", "20"))) + o += mkBool(b.get("walkable", False)) + o += bytes(3) + + o += fxconv.sym("Nooncraft_blockInfoCount") + o += fxconv.u32(len(data)) + return o diff --git a/src/block.cpp b/src/block.cpp new file mode 100644 index 0000000..351c089 --- /dev/null +++ b/src/block.cpp @@ -0,0 +1,17 @@ +#include "block.h" + +extern "C" { +extern BlockInfo Nooncraft_blockInfo[]; +extern int Nooncraft_blockInfoCount; +} + +namespace Nooncraft { + +BlockInfo *getBlockInfo(Block b) +{ + if(b >= Nooncraft_blockInfoCount) + return nullptr; + return &Nooncraft_blockInfo[b]; +} + +} /* namespace Nooncraft */ diff --git a/src/block.h b/src/block.h new file mode 100644 index 0000000..d32a3b4 --- /dev/null +++ b/src/block.h @@ -0,0 +1,35 @@ +// nooncraft.block: Block data in world map and general block information + +#pragma once +#include "item.h" +#include "render.h" +#include + +using Block = uint16_t; + +enum class Breakability: uint8_t { + Fluid, + ByAnything, + ByTool, + Unbreakable, +}; + +/* General information on blocks */ +struct BlockInfo +{ + char const *name; + + // TODO: Upgrade to random and/or dimension-specific sprites + GlyphCluster cluster; + + Breakability breakability; + ToolKind toolKind; + int16_t baseBreakDurationTicks; + bool walkable; +}; + +namespace Nooncraft { + +BlockInfo *getBlockInfo(Block b); + +} /* namespace Nooncraft */ diff --git a/src/graphics.cpp b/src/graphics.cpp new file mode 100644 index 0000000..02ceb78 --- /dev/null +++ b/src/graphics.cpp @@ -0,0 +1,19 @@ +#include "graphics.h" +#include "render.h" + +namespace Nooncraft { + +void renderCamera(int x, int y, Camera *camera) +{ + if(!camera) + return; + + WorldRect r = camera->region; + for(int dy = r.ymin; dy <= r.ymax; dy++) + for(int dx = r.xmin; dx <= r.xmax; dx++) { + GlyphCluster c('@', '#', '~', '&'); + renderCluster(x+2*dx, y+2*dy, c); + } +} + +} /* namespace Nooncraft */ diff --git a/src/graphics.h b/src/graphics.h new file mode 100644 index 0000000..e807159 --- /dev/null +++ b/src/graphics.h @@ -0,0 +1,24 @@ +// nooncraft.graphics: Application-level rendering logic + +#pragma once +#include "world.h" + +struct Camera +{ + Camera(World const *w): world {w}, center {0,0}, region {-1,1,-1,1} {} + + /* Underlying world object */ + World const *world; + + /* Coordinates of the center point */ + WorldCoord center; + /* Local rectangle around center at (0,0). This influences how much of the + screen the camera will take up when rendered */ + WorldRect region; +}; + +namespace Nooncraft { + +void renderCamera(int x, int y, Camera *camera); + +} /* namespace Nooncraft */ diff --git a/src/item.h b/src/item.h new file mode 100644 index 0000000..3b7afdd --- /dev/null +++ b/src/item.h @@ -0,0 +1,8 @@ +// nooncraft.item: Item storage and properties + +#pragma once +#include + +enum class ToolKind: uint8_t { + Pickaxe, Axe, Shovel, Hoe, +}; diff --git a/src/main.c b/src/main.c deleted file mode 100644 index 1a9e9b4..0000000 --- a/src/main.c +++ /dev/null @@ -1,12 +0,0 @@ -#include -#include - -int main(void) -{ - dclear(C_WHITE); - dtext(1, 1, C_BLACK, "Sample fxSDK add-in."); - dupdate(); - - getkey(); - return 1; -} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..336bf9d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,53 @@ +#include "render.h" +#include "world.h" +#include "graphics.h" +#include +#include + +int main(void) +{ + World *world = Nooncraft::mkWorld(128, 128); + if(!world) + return 0; + + Nooncraft::genWorld(world); + + Camera *camera = new Camera(world); + camera->region = WorldRect(-13, 14, -6, 6); + + // - + + char separator[57]; + memset(separator, '-', 56); + separator[56] = 0; + + renderClear(); + Nooncraft::renderCamera(26, 12, camera); + + // HUD + + GlyphCluster invLeft(' ', GLYPH_PARALLEL_TO, ' ', GLYPH_PARALLEL_TO); + GlyphCluster invRight(GLYPH_PARALLEL_TO, ' ', GLYPH_PARALLEL_TO, ' '); + int hudY = 27; + + renderText(0, hudY, separator); + renderText(24, hudY+1, "COBBLESTONE"); + renderCluster(0, hudY+1, invLeft); + renderCluster(2, hudY+1, Nooncraft::getBlockInfo(2)->cluster); + renderCluster(20, hudY+1, invRight); + renderText(2, hudY+3, "1 2 3 4 5 6 7 8 9"); + + // - + + renderText(0, 0, "NOONCRAFT"); + renderUpdate(); + + getkey(); + + //- + + delete camera; + delete world; + + return 1; +} diff --git a/src/render.cpp b/src/render.cpp new file mode 100644 index 0000000..dea9890 --- /dev/null +++ b/src/render.cpp @@ -0,0 +1,87 @@ +#include "render.h" +#include +#include +#include + +extern "C" { +extern font_t font_nooncraft; +} + +GCONSTRUCTOR +static void init_font(void) +{ + dfont(&font_nooncraft); +} + +static bool nightMode = false; + +static inline int fg() +{ + return nightMode ? C_WHITE : C_BLACK; +} +static inline int bg() +{ + return nightMode ? C_BLACK : C_WHITE; +} +static inline void dglyph(int x, int y, Glyph g) +{ + dtext_opt(x, y, fg(), bg(), DTEXT_LEFT, DTEXT_TOP, &g, 1); +} + +void renderSetNight(bool night) +{ + nightMode = night; +} + +void renderClear(void) +{ + dclear(bg()); +} + +void renderUpdate(void) +{ + dupdate(); +} + +static int cellX(int x, bool text) +{ + return 4 + 7 * x - (!text && (x & 1)); +} + +static int cellY(int y, bool text) +{ + (void)text; + return 2 + 7 * y - (y & 1); +} + +void renderGlyph(int x, int y, Glyph g) +{ + dglyph(cellX(x, true), cellY(y, true), g); +} + +void renderCluster(int x, int y, GlyphCluster c) +{ + int px = cellX(x & -2, false); + int py = cellY(y & -2, false); + + dglyph(px, py, c.glyphs[0]); + dglyph(px+6, py, c.glyphs[1]); + dglyph(px, py+6, c.glyphs[2]); + dglyph(px+6, py+6, c.glyphs[3]); +} + +void renderText(int x, int y, char const *text) +{ + for(int i = 0; text[i]; i++) + renderGlyph(x + i, y, text[i]); +} + +void renderFormat(int x, int y, char const *fmt, ...) +{ + static char str[128]; + va_list args; + va_start(args, fmt); + vsnprintf(str, sizeof str, fmt, args); + va_end(args); + renderText(x, y, str); +} diff --git a/src/render.h b/src/render.h new file mode 100644 index 0000000..47bc769 --- /dev/null +++ b/src/render.h @@ -0,0 +1,69 @@ +// nooncraft.render: Grid-like text rendering +// +// Owing to the CPC constraint, the entire rendering system for this game is +// just text with a single font. We make two adjustments on the grid model: +// +// 1. We group rows and columns by pairs, so that the row/column spacing +// alternates between 1 and 3. This allows blocks to be represented by +// "clusters" of 4 characters. +// 2. To keep text natural, we provide a text function which forces column +// spacing to 2, offsetting every other character by 1 pixel. Rows remain +// irregularly spaced. +//--- + +#pragma once + +using Glyph = char; + +/* Non-ASCII glyphs */ +enum { + GLYPH_FOUR_DOT = 1, /* U+2058 FOUR DOT PUNCTUATION */ + GLYPH_MIDDLE_DOT = 2, /* U+00B7 MIDDLE DOT */ + GLYPH_PARALLEL_TO = 3, /* U+2225 PARALLEL TO */ + GLYPH_CURRENCY_SIGN = 4, /* U+00A4 CURRENCY SIGN */ + GLYPH_NORTH_EAST_ARROW = 5, /* U+2197 NORTH EAST ARROW */ + GLYPH_RIGHT_GUILLEMET = 6, /* U+00BB RIGHT GUILLEMET */ + GLYPH_DOWN_ARROW = 7, /* U+2193 DOWNARDS ARROW */ + GLYPH_I_CIRCUMFLEX = 8, /* U+00EE SMALL LATIN I WITH CIRCUMFLEX */ + GLYPH_INTEGRAL = 9, /* U+222B INTEGRAL */ + GLYPH_SMALL_SQUARE = 10, /* U+25AA BLACK SMALL SQUARE */ + GLYPH_STAR = 11, /* U+2605 BLACK STAR */ +}; + +/* A cluster of 4 glyphs. Can be conversion-constructed from a multi-byte + character literal like 'AXE '. */ +struct GlyphCluster +{ + GlyphCluster(Glyph g1, Glyph g2, Glyph g3, Glyph g4): + glyphs {g1, g2, g3, g4} + {} + + GlyphCluster(int mbliteral) { + this->glyphs[0] = mbliteral >> 24; + this->glyphs[1] = (mbliteral >> 16) & 0xff; + this->glyphs[2] = (mbliteral >> 8) & 0xff; + this->glyphs[3] = mbliteral & 0xff; + } + + Glyph glyphs[4]; +}; + +/* Enable or disable night mode (black-on-white). */ +void renderSetNight(bool night); +/* Clear the screen. */ +void renderClear(void); +/* Update the screen. */ +void renderUpdate(void); + +/* Render a single character with text alignment (2-pixel column spacing). + The position (x,y) is counted in characters units with (0,0) top-left. */ +void renderGlyph(int x, int y, Glyph g); + +/* Render a 2x2 cluster of 4 characters. The position (x,y) should be even on + both axes. */ +void renderCluster(int x, int y, GlyphCluster c); + +/* Render a string with text alignment. */ +void renderText(int x, int y, char const *text); +/* Same with printf() formatting. */ +void renderFormat(int x, int y, char const *fmt, ...); diff --git a/src/world.cpp b/src/world.cpp new file mode 100644 index 0000000..9b8a51e --- /dev/null +++ b/src/world.cpp @@ -0,0 +1,71 @@ +#include "world.h" +#include +#include + +WorldCoord WorldCoord::Up(0, -1); +WorldCoord WorldCoord::Down(0, 1); +WorldCoord WorldCoord::Left(-1, 0); +WorldCoord WorldCoord::Right(1, 0); + +namespace Nooncraft { + +World *mkWorld(int width, int height) +{ + WorldRect l, br; + + World *w = (World *)malloc(sizeof *w); + if(!w) goto fail; + + w->cells = (Block *)malloc(width * height * sizeof *w->cells); + if(!w->cells) goto fail; + + l = WorldRect( + -width / 2, width - width / 2, + -height / 2, height - height / 2); + br = WorldRect( + l.xmin + 2, l.xmax - 2, + l.ymin + 2, l.ymax - 2); + + w->limits = l; + w->worldBorder = br; + return w; + +fail: + if(w) + free(w->cells); + free(w); + return nullptr; +} + +void genWorld(World *w) +{ + memset(w->cells, 0, w->cellDataSize()); + + int x1 = w->worldBorder.xmin + 1; + int x2 = w->worldBorder.xmax - 1; + int y1 = w->worldBorder.ymin + 1; + int y2 = w->worldBorder.ymax - 1; + + for(int x = x1; x <= x2; x++) { + w->cellAt(x, y1) = 1; // TODO: Use cobblestone enum? + w->cellAt(x, y2) = 1; + } + for(int y = y1; y <= y2; y++) { + w->cellAt(x1, y) = 1; + w->cellAt(x2, y) = 1; + } + + for(int x = -1; x <= +1; x++) + for(int y = -1; y <= +1; y++) { + if(x || y) + w->cellAt(x, y) = 1; + } +} + +void freeWorld(World *w) +{ + free(w->cells); + free(w); +} + +} /* namespace Nooncraft */ diff --git a/src/world.h b/src/world.h new file mode 100644 index 0000000..8c651cc --- /dev/null +++ b/src/world.h @@ -0,0 +1,92 @@ +// nooncraft.world: World data and structures + +#pragma once +#include "block.h" +#include +#include + +struct WorldCoord +{ + int16_t x, y; + + constexpr WorldCoord(): x {0}, y {0} {} + constexpr WorldCoord(int _x, int _y): x {(int16_t)_x}, y {(int16_t)_y} {} + + inline constexpr WorldCoord &operator+=(WorldCoord const &other) { + this->x += other.x; + this->y += other.y; + return *this; + } + inline constexpr WorldCoord operator-=(WorldCoord const &other) { + this->x -= other.x; + this->y -= other.y; + return *this; + } + + static WorldCoord Up, Down, Left, Right; +}; + +inline constexpr WorldCoord operator+(WorldCoord l, WorldCoord const &r) { + return (l += r); +} +inline constexpr WorldCoord operator-(WorldCoord l, WorldCoord const &r) { + return (l -= r); +} + +struct WorldRect +{ + /* All included */ + int16_t xmin, xmax; + int16_t ymin, ymax; + + constexpr WorldRect(): xmin {0}, xmax {0}, ymin {0}, ymax {0} {} + constexpr WorldRect(int _xmin, int _xmax, int _ymin, int _ymax): + xmin {(int16_t)_xmin}, xmax {(int16_t)_xmax}, + ymin {(int16_t)_ymin}, ymax {(int16_t)_ymax} {} + + int width() const { + return xmax - xmin + 1; + } + int height() const { + return ymax - ymin + 1; + } +}; + +struct World +{ + /* Map data limits (ie. how far blocks exist) */ + WorldRect limits; + /* World border position */ + WorldRect worldBorder; + + /* Block cells in row-major order */ + Block *cells; + + int cellCount() const { + return limits.width() * limits.height(); + } + size_t cellDataSize() const { + return cellCount() * sizeof(*cells); + } + GINLINE Block const &cellAt(int16_t x, int16_t y) const { + int ix = (x + limits.xmin) + (y + limits.ymin) * limits.width(); + return cells[ix]; + } + GINLINE Block &cellAt(int16_t x, int16_t y) { + int ix = (x + limits.xmin) + (y + limits.ymin) * limits.width(); + return cells[ix]; + } +}; + +namespace Nooncraft { + +/* Make an empty uninitialized world map. */ +World *mkWorld(int width, int height); + +/* Run the world generator, initializing the entire world. */ +void genWorld(World *w); + +/* Free a world object. */ +void freeWorld(World *w); + +} /* namespace Nooncraft */