From 3fd0efb4d8db36cd113ba4c0972c06e40d6d5fe6 Mon Sep 17 00:00:00 2001
From: Dominic Masters <dominic@domsplace.com>
Date: Sun, 17 Oct 2021 11:08:40 -0700
Subject: [PATCH] Utils example

---
 CMakeLists.txt                                |  28 +++--
 src/engine/engine.c                           |   8 +-
 src/game/CMakeLists.txt                       |   2 +-
 tools/CMakeLists.txt                          |   6 +
 tools/utils/args.js                           |  12 ++
 tools/utils/file.js                           |  19 ++++
 tools/utils/image.js                          | 103 ++++++++++++++++++
 tools/vn/CMakeLists.txt                       |  12 ++
 tools/vn/character-sheet-generator.js         |  93 ++++++++++++++++
 tools/vn/character-sheet-maker/CMakeLists.txt |  24 ----
 tools/vn/character-sheet-maker/main.c         |  12 --
 tools/vn/character-sheet-maker/main.h         |  11 --
 12 files changed, 272 insertions(+), 58 deletions(-)
 create mode 100644 tools/CMakeLists.txt
 create mode 100644 tools/utils/args.js
 create mode 100644 tools/utils/file.js
 create mode 100644 tools/utils/image.js
 create mode 100644 tools/vn/CMakeLists.txt
 create mode 100644 tools/vn/character-sheet-generator.js
 delete mode 100644 tools/vn/character-sheet-maker/CMakeLists.txt
 delete mode 100644 tools/vn/character-sheet-maker/main.c
 delete mode 100644 tools/vn/character-sheet-maker/main.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index ab2bc810..8b2f2429 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,24 +15,29 @@ add_compile_definitions(
 )
 
 # Do initial set up depending on the build target type.
-if(TARGET_TYPE STREQUAL tool)
-  set(TARGET_NAME tool)
-elseif(TARGET_TYPE STREQUAL test)
+if(TARGET_TYPE STREQUAL test)
   set(TARGET_NAME test)
-else()
+elseif(TARGET_TYPE STREQUAL game)
   set(TARGET_NAME ${TARGET_GAME})
+else()
+  message(FATAL_ERROR "Missing or invalid definition of TARGET_TYPE")
 endif()
 
 # Set up the project
 project(${TARGET_NAME} VERSION 1.0)
 add_executable(${PROJECT_NAME})
 
-# Now change sources depending on the target type
-if(TARGET_TYPE STREQUAL tool)
+# Variables
+SET(ROOT_DIR "${CMAKE_SOURCE_DIR}")
+set(TOOLS_DIR "${ROOT_DIR}/tools")
 
-elseif(TARGET_TYPE STREQUAL test)
+# Include tools
+add_subdirectory(tools)
+
+# Now change sources depending on the target type
+if(TARGET_TYPE STREQUAL test)
   add_subdirectory(test)
-else()
+elseif(TARGET_TYPE STREQUAL game)
   if(TARGET_GAME STREQUAL poker)
     add_compile_definitions(
       GAME_NAME="Penny's Poker"
@@ -43,8 +48,13 @@ else()
       GAME_DISPOSE=pokerGameDispose
       GAME_VERSION=1.0
     )
-  endif()
 
+    set(DIR_CHARS assets/poker/characters/penny)
+
+    tool_vn_character(penny ${DIR_CHARS}/character.xml ${DIR_CHARS}/bruh.png)
+
+    add_dependencies(${PROJECT_NAME} penny)
+  endif()
   add_subdirectory(client)
 endif()
 
diff --git a/src/engine/engine.c b/src/engine/engine.c
index fd1334e6..9646071d 100644
--- a/src/engine/engine.c
+++ b/src/engine/engine.c
@@ -9,7 +9,13 @@
 
 void engineInit(engine_t *engine) {
   randSeed(123);
-  engine->name = GAME_NAME;
+  
+  #if defined(GAME_NAME)
+    engine->name = GAME_NAME;
+  #else
+    engine->name = "Dawn";
+  #endif
+
   clientInit(&engine->client);
   epochInit(&engine->time);
   renderInit();
diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt
index 652e62a4..2e28eb7c 100644
--- a/src/game/CMakeLists.txt
+++ b/src/game/CMakeLists.txt
@@ -11,7 +11,7 @@
 file(GLOB GAME_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.c)
 file(GLOB GAME_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
 file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET_GAME}/*.c)
-file(GLOB_RECURSE HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET_GAME}*.h)
+file(GLOB_RECURSE HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET_GAME}/*.h)
 
 target_sources(${PROJECT_NAME}
   PRIVATE
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
new file mode 100644
index 00000000..d6419e6e
--- /dev/null
+++ b/tools/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright (c) 2021 Dominic Msters
+# 
+# This software is released under the MIT License.
+# https://opensource.org/licenses/MIT
+
+add_subdirectory(vn)
\ No newline at end of file
diff --git a/tools/utils/args.js b/tools/utils/args.js
new file mode 100644
index 00000000..94efab05
--- /dev/null
+++ b/tools/utils/args.js
@@ -0,0 +1,12 @@
+const args = process.argv.splice(2).reduce((x,y) => {
+  const bits = y.split('=');
+  const name = bits[0].replace('--', '');
+  const val = bits[1];
+
+  x[name] = val;
+  return x;
+}, {});
+
+module.exports = {
+  args
+};
\ No newline at end of file
diff --git a/tools/utils/file.js b/tools/utils/file.js
new file mode 100644
index 00000000..bb172be2
--- /dev/null
+++ b/tools/utils/file.js
@@ -0,0 +1,19 @@
+const path = require('path');
+const fs = require('fs');
+
+const mkdirp = dir => {
+  const resolved = path.resolve(dir);
+  const resolvedDir = path.dirname(resolved);
+  const bits = resolvedDir.split(path.sep);
+  let running = '';
+
+  bits.forEach(bit => {
+    running += bit;
+    if(!fs.existsSync(running)) fs.mkdirSync(running);
+    running += path.sep;
+  });
+}
+
+module.exports = {
+  mkdirp
+}
\ No newline at end of file
diff --git a/tools/utils/image.js b/tools/utils/image.js
new file mode 100644
index 00000000..5c4b7e61
--- /dev/null
+++ b/tools/utils/image.js
@@ -0,0 +1,103 @@
+const { PNG } = require("pngjs");
+const path = require('path');
+const fs = require('fs');
+
+/**
+ * Loads an image into memory.
+ * @param image Image to load
+ * @returns A promise that resolves to the loaded image.
+ */
+const imageLoad = (image) => new Promise(resolve => {
+  fs.createReadStream(image)
+    .pipe(new PNG({ filterType: 4 }))
+    .on("parsed", function () {
+      // Normalize
+      const pixels = [];
+      for(let y = 0; y < this.height; y++) {
+        for(let x = 0; x < this.width; x++) {
+          const idx = (this.width * y + x) << 2;
+          const r = this.data[idx];
+          const g = this.data[idx + 1];
+          const b = this.data[idx + 2];
+          const a = this.data[idx + 3];
+
+          pixels.push({ r, g, b, a });
+        }
+      }
+      resolve({ pixels, width: this.width, height: this.height });
+    })
+  ;
+});
+
+/**
+ * Writes an image to an output file.
+ * @param image Image to write.
+ * @param file File to write to.
+ * @returns A promise that, when resolved, has saved the image.
+ */
+const imageWrite = (image, file) => new Promise(resolve => {
+  const png = new PNG({ width: image.width, height: image.height });
+  png.width = image.width;
+  png.height = image.height;
+  
+  for(let y = 0; y < image.height; y++) {
+    for(let x = 0; x < image.width; x++) {
+      const i = (image.width * y + x);
+      const idx = i << 2;
+
+      const pixel = image.pixels[i];
+      png.data[idx] = pixel.r;
+      png.data[idx + 1] = pixel.g;
+      png.data[idx + 2] = pixel.b;
+      png.data[idx + 3] = pixel.a;
+    }
+  }
+
+  png.pack().pipe(fs.createWriteStream(file))
+    .on('close', () => resolve(true))
+  ;
+});
+
+/**
+ * Creates a blank image
+ * @param width Width of the image.
+ * @param height Height of the image.
+ * @param fill Optional pixel to fill with, defaults to 0,0,0,0
+ * @returns The newly created image.
+ */
+const imageCreate = (width, height, pixel) => {
+  if(!pixel || !pixel.r) pixel = { r:0, g:0, b:0, a:0 };
+  const pixels = [];
+  for(let i = 0; i < width * height; i++) pixels.push({ ...pixel });
+  return { pixels, width, height };
+}
+
+/**
+ * Copies an area of a source image into a target image.
+ * @param target Target image to copy into.
+ * @param source Source image to copy from.
+ * @param tx Target X position to copy into
+ * @param ty Target Y position to copy into
+ * @param sub Optional source area to use, defined as { x, y, width, height }.
+ */
+const imageCopy = (target, source, tx, ty, sub) => {
+  if(!sub) sub = { x: 0, y: 0, width: source.width, height: source.height };
+
+  for(let x = sub.x; x < sub.x+sub.width; x++) {
+    for(let y = sub.y; y < sub.y+sub.height; y++) {
+      let absX = x - sub.x + tx;
+      let absY = y - sub.y + ty;
+      if(absX > target.width || absY > target.height) continue;
+      let ti = absY * target.width + absX;
+      let si = y * source.width + x;
+      target.pixels[ti] = { ...source.pixels[si] };
+    }
+  }
+}
+
+module.exports = {
+  imageWrite,
+  imageCreate,
+  imageLoad,
+  imageCopy
+}
\ No newline at end of file
diff --git a/tools/vn/CMakeLists.txt b/tools/vn/CMakeLists.txt
new file mode 100644
index 00000000..f3c7b44e
--- /dev/null
+++ b/tools/vn/CMakeLists.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2021 Dominic Msters
+# 
+# This software is released under the MIT License.
+# https://opensource.org/licenses/MIT
+
+
+function(tool_vn_character DEP_NAME IN OUT)
+  add_custom_target(${DEP_NAME}
+    COMMAND node ${TOOLS_DIR}/vn/character-sheet-generator.js --root="${ROOT_DIR}" --in="${ROOT_DIR}/${IN}" --out="${OUT}"
+    COMMENT "Adding VN Character ${FILE_NAME}"
+  )
+endfunction()
\ No newline at end of file
diff --git a/tools/vn/character-sheet-generator.js b/tools/vn/character-sheet-generator.js
new file mode 100644
index 00000000..b0f010e8
--- /dev/null
+++ b/tools/vn/character-sheet-generator.js
@@ -0,0 +1,93 @@
+const path = require('path');
+const { imageCreate, imageWrite, imageLoad, imageCopy } = require('./../utils/image');
+const fs = require('fs');
+const xml = require('xml-js');
+const { args } = require('./../utils/args');
+const { mkdirp } = require('../utils/file');
+
+// Parse Args
+if(!args.root) throw new Error(`Missing root argument`);
+if(!args.in) throw new Error(`Missing in argument`);
+if(!args.out) throw new Error(`Missing out argument`);
+if(!args.in.endsWith('xml')) throw new Error(`Invalid in XML`);
+if(!args.out.endsWith('png')) throw new Error(`Invalid out PNG`);
+
+// Determine in and out.
+const root = path.resolve(args.root);
+const file = path.resolve(args.in);
+if(!fs.existsSync(file)) throw new Error(`Could not find ${file}`);
+const outFile = path.resolve(args.out);
+if(fs.existsSync(outFile)) return;
+
+// Load XML
+const data = xml.xml2js(fs.readFileSync(file, 'utf-8'));
+const [ character ] = data.elements;
+
+// Validate file.
+if(!character.attributes.context) throw new Error(`Missing context`)
+const dir = path.resolve(root, 'assets', character.attributes.context);
+
+// Parse base and layers
+const base = character.elements.find(e => e.name == 'base').attributes;
+if(!base) throw new Error(`Failed to find base`);
+const layers = character.elements
+  .filter(e => e.name == 'layer')
+  .map(e => e.attributes)
+  .map(e => ({
+    ...e,
+    x: parseInt(e.x),
+    y: parseInt(e.y),
+    width: parseInt(e.width),
+    height: parseInt(e.height)
+  }))
+;
+
+(async () => {
+  // Load the base
+  const baseImage = await imageLoad(path.join(dir, base.file));
+
+  let columnsMax = 0;
+  let widthMax = 0;
+  layers.forEach((layer,row) => {
+    if(!layer.width || !layer.height || !layer.x || !layer.y) {
+      throw new Error(`Missing layer info`);
+    }
+
+    const layerDir = path.join(dir, layer.directory);
+    const scan = fs.readdirSync(layerDir);
+    columnsMax = Math.max(scan.length, columnsMax);
+    widthMax = Math.max(widthMax, layer.width);
+  });
+
+  // Create the output buffer
+  const out = imageCreate(
+    baseImage.width + (columnsMax * widthMax),
+    baseImage.height
+  );
+
+  // Copy the base
+  imageCopy(out, baseImage, 0, 0);
+  
+  // Now begin copying the children, row is defined by the directory
+  let y = 0;
+  for(let row = 0; row < layers.length; row++) {
+    const layer = layers[row];
+    const layerDir = path.join(dir, layer.directory);
+    const scan = fs.readdirSync(layerDir);
+
+    // Column defined by the file index
+    for(let col = 0; col < scan.length; col++) {
+      const img = await imageLoad(path.join(layerDir, scan[col]));
+      console.log('Copying', scan[col]);
+      imageCopy(out, img,
+        baseImage.width+(col*layer.width), y,
+        layer
+      );
+    }
+
+    y += layer.height;
+  }
+
+  mkdirp(outFile);
+  await imageWrite(out, outFile);
+})().catch(console.error);
\ No newline at end of file
diff --git a/tools/vn/character-sheet-maker/CMakeLists.txt b/tools/vn/character-sheet-maker/CMakeLists.txt
deleted file mode 100644
index f49e3604..00000000
--- a/tools/vn/character-sheet-maker/CMakeLists.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Copyright (c) 2021 Dominic Msters
-# 
-# This software is released under the MIT License.
-# https://opensource.org/licenses/MIT
-
-# Definitions
-
-# Libraries
-
-# Sources
-file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.c)
-file(GLOB_RECURSE HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
-
-target_sources(${PROJECT_NAME}
-  PRIVATE
-    ${SOURCES}
-    ${HEADERS}
-)
-
-# Includes
-target_include_directories(${PROJECT_NAME}
-  PUBLIC
-    ${CMAKE_CURRENT_LIST_DIR}
-)
\ No newline at end of file
diff --git a/tools/vn/character-sheet-maker/main.c b/tools/vn/character-sheet-maker/main.c
deleted file mode 100644
index 94841dce..00000000
--- a/tools/vn/character-sheet-maker/main.c
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * Copyright (c) 2021 Dominic Masters
- * 
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#include "character-sheet-maker.h"
-
-int32_t main(int32_t argc, char *argv[]) {
-  
-}
\ No newline at end of file
diff --git a/tools/vn/character-sheet-maker/main.h b/tools/vn/character-sheet-maker/main.h
deleted file mode 100644
index 71ddb411..00000000
--- a/tools/vn/character-sheet-maker/main.h
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * Copyright (c) 2021 Dominic Masters
- * 
- * This software is released under the MIT License.
- * https://opensource.org/licenses/MIT
- */
-
-#pragma once
-
-
-int32_t main(int32_t argc, char *argv[]);
\ No newline at end of file