M .gitignore => .gitignore +5 -1
@@ 2,4 2,8 @@ target
client/pkg
web/dist
.idea
-assets/svg/*.png>
\ No newline at end of file
+assets/svg/*.png
+build.ninja
+.ninja_log
+assets/final
+assets/dist<
\ No newline at end of file
A spacetime/ninja_syntax.py => spacetime/ninja_syntax.py +199 -0
@@ 0,0 1,199 @@
+#!/usr/bin/python
+
+# Copyright 2011 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Python module for generating .ninja files.
+
+Note that this is emphatically not a required piece of Ninja; it's
+just a helpful utility for build-file-generation systems that already
+use Python.
+"""
+
+import re
+import textwrap
+
+def escape_path(word):
+ return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
+
+class Writer(object):
+ def __init__(self, output, width=78):
+ self.output = output
+ self.width = width
+
+ def newline(self):
+ self.output.write('\n')
+
+ def comment(self, text):
+ for line in textwrap.wrap(text, self.width - 2, break_long_words=False,
+ break_on_hyphens=False):
+ self.output.write('# ' + line + '\n')
+
+ def variable(self, key, value, indent=0):
+ if value is None:
+ return
+ if isinstance(value, list):
+ value = ' '.join(filter(None, value)) # Filter out empty strings.
+ self._line('%s = %s' % (key, value), indent)
+
+ def pool(self, name, depth):
+ self._line('pool %s' % name)
+ self.variable('depth', depth, indent=1)
+
+ def rule(self, name, command, description=None, depfile=None,
+ generator=False, pool=None, restat=False, rspfile=None,
+ rspfile_content=None, deps=None):
+ self._line('rule %s' % name)
+ self.variable('command', command, indent=1)
+ if description:
+ self.variable('description', description, indent=1)
+ if depfile:
+ self.variable('depfile', depfile, indent=1)
+ if generator:
+ self.variable('generator', '1', indent=1)
+ if pool:
+ self.variable('pool', pool, indent=1)
+ if restat:
+ self.variable('restat', '1', indent=1)
+ if rspfile:
+ self.variable('rspfile', rspfile, indent=1)
+ if rspfile_content:
+ self.variable('rspfile_content', rspfile_content, indent=1)
+ if deps:
+ self.variable('deps', deps, indent=1)
+
+ def build(self, outputs, rule, inputs=None, implicit=None, order_only=None,
+ variables=None, implicit_outputs=None, pool=None, dyndep=None):
+ outputs = as_list(outputs)
+ out_outputs = [escape_path(x) for x in outputs]
+ all_inputs = [escape_path(x) for x in as_list(inputs)]
+
+ if implicit:
+ implicit = [escape_path(x) for x in as_list(implicit)]
+ all_inputs.append('|')
+ all_inputs.extend(implicit)
+ if order_only:
+ order_only = [escape_path(x) for x in as_list(order_only)]
+ all_inputs.append('||')
+ all_inputs.extend(order_only)
+ if implicit_outputs:
+ implicit_outputs = [escape_path(x)
+ for x in as_list(implicit_outputs)]
+ out_outputs.append('|')
+ out_outputs.extend(implicit_outputs)
+
+ self._line('build %s: %s' % (' '.join(out_outputs),
+ ' '.join([rule] + all_inputs)))
+ if pool is not None:
+ self._line(' pool = %s' % pool)
+ if dyndep is not None:
+ self._line(' dyndep = %s' % dyndep)
+
+ if variables:
+ if isinstance(variables, dict):
+ iterator = iter(variables.items())
+ else:
+ iterator = iter(variables)
+
+ for key, val in iterator:
+ self.variable(key, val, indent=1)
+
+ return outputs
+
+ def include(self, path):
+ self._line('include %s' % path)
+
+ def subninja(self, path):
+ self._line('subninja %s' % path)
+
+ def default(self, paths):
+ self._line('default %s' % ' '.join(as_list(paths)))
+
+ def _count_dollars_before_index(self, s, i):
+ """Returns the number of '$' characters right in front of s[i]."""
+ dollar_count = 0
+ dollar_index = i - 1
+ while dollar_index > 0 and s[dollar_index] == '$':
+ dollar_count += 1
+ dollar_index -= 1
+ return dollar_count
+
+ def _line(self, text, indent=0):
+ """Write 'text' word-wrapped at self.width characters."""
+ leading_space = ' ' * indent
+ while len(leading_space) + len(text) > self.width:
+ # The text is too wide; wrap if possible.
+
+ # Find the rightmost space that would obey our width constraint and
+ # that's not an escaped space.
+ available_space = self.width - len(leading_space) - len(' $')
+ space = available_space
+ while True:
+ space = text.rfind(' ', 0, space)
+ if (space < 0 or
+ self._count_dollars_before_index(text, space) % 2 == 0):
+ break
+
+ if space < 0:
+ # No such space; just use the first unescaped space we can find.
+ space = available_space - 1
+ while True:
+ space = text.find(' ', space + 1)
+ if (space < 0 or
+ self._count_dollars_before_index(text, space) % 2 == 0):
+ break
+ if space < 0:
+ # Give up on breaking.
+ break
+
+ self.output.write(leading_space + text[0:space] + ' $\n')
+ text = text[space+1:]
+
+ # Subsequent lines are continuations, so indent them.
+ leading_space = ' ' * (indent+2)
+
+ self.output.write(leading_space + text + '\n')
+
+ def close(self):
+ self.output.close()
+
+
+def as_list(input):
+ if input is None:
+ return []
+ if isinstance(input, list):
+ return input
+ return [input]
+
+
+def escape(string):
+ """Escape a string such that it can be embedded into a Ninja file without
+ further interpretation."""
+ assert '\n' not in string, 'Ninja syntax does not allow newlines'
+ # We only have one special metacharacter: '$'.
+ return string.replace('$', '$$')
+
+
+def expand(string, vars, local_vars={}):
+ """Expand a string containing $vars as Ninja would.
+
+ Note: doesn't handle the full Ninja variable syntax, but it's enough
+ to make configure.py's use of it work.
+ """
+ def exp(m):
+ var = m.group(1)
+ if var == '$':
+ return '$'
+ return local_vars.get(var, vars.get(var, ''))
+ return re.sub(r'\$(\$|\w*)', exp, string)
A spacetime/spacetime.py => spacetime/spacetime.py +123 -0
@@ 0,0 1,123 @@
+import sys
+from ninja_syntax import Writer
+import os
+
+
+def scan_assets(build_root):
+ print(f'[spacetime] Scanning {build_root}/assets/src for assets')
+ assets = []
+ for entry in os.scandir(f'{build_root}/assets/src'):
+ if entry.is_file() and entry.name.endswith('.ink.svg'):
+ assets.append(f'{build_root}/assets/src/{entry.name}')
+ print(f'[spacetime] Found {len(assets)} assets')
+ return assets
+
+
+default_asset_size = 512
+asset_override = {
+ 'earth.ink.svg': 2048
+}
+
+
+def gen_inkscape_rules_for_asset_size(size, writer):
+ writer.rule(f'inkscape_{size}px_full', f'inkscape -w {size * 1} -h {size * 1} $in -o $out')
+ writer.rule(f'inkscape_{size}px_375', f'inkscape -w {int(size * 0.375)} -h {int(size * 0.375)} $in -o $out')
+ writer.rule(f'inkscape_{size}px_125', f'inkscape -w {int(size * 0.125)} -h {int(size * 0.125)} $in -o $out')
+
+
+def gen_inkscape_rules_for_asset_sizes(writer):
+ gen_inkscape_rules_for_asset_size(default_asset_size, writer)
+ for override in asset_override:
+ gen_inkscape_rules_for_asset_size(asset_override[override], writer)
+
+
+def asset_size(asset):
+ if asset.split('/')[-1] in asset_override:
+ return asset_override[asset.split('/')[-1]]
+ else:
+ return default_asset_size
+
+
+def gen_inkscape_rules_for_asset(root, asset, writer, files_375, files_full, files_125):
+ in_file = asset
+ out_file_name = asset.split('.')[0].split('/')[-1]
+
+ out_full = f'{root}/assets/final/full/{out_file_name}.png'
+ files_full.append(out_full)
+ rule_full = f'inkscape_{asset_size(asset)}px_full'
+
+ out_375 = f'{root}/assets/final/375/{out_file_name}.png'
+ files_375.append(out_375)
+ rule_375 = f'inkscape_{asset_size(asset)}px_375'
+
+ out_125 = f'{root}/assets/final/125/{out_file_name}.png'
+ files_125.append(out_125)
+ rule_125 = f'inkscape_{asset_size(asset)}px_125'
+
+ writer.build([out_full], rule_full, [in_file])
+ writer.build([out_375], rule_375, [in_file])
+ writer.build([out_125], rule_125, [in_file])
+
+
+def gen_inkscape(root, assets, writer, files_375, files_full, files_125):
+ gen_inkscape_rules_for_asset_sizes(writer)
+
+ for asset in assets:
+ gen_inkscape_rules_for_asset(root, asset, writer, files_375, files_full, files_125)
+
+
+def gen_packers(root, writer, files_375, files_full, files_125):
+ # sheep pack assets/final/full/*.png -f amethyst_named -o assets/dist/spritesheet-full
+ writer.rule(f'pack', 'sheep pack -f amethyst_named -o $out $in && touch $out')
+
+ writer.build(f'{root}/assets/dist/spritesheet-full', 'pack', inputs=files_full,
+ implicit_outputs=[f'{root}/assets/dist/spritesheet-full.png',
+ f'{root}/assets/dist/spritesheet-full.ron'])
+ writer.build(f'asset-full', 'phony', inputs=[f'{root}/assets/dist/spritesheet-full'])
+
+ writer.build(f'{root}/assets/dist/spritesheet-375', 'pack', inputs=files_375,
+ implicit_outputs=[f'{root}/assets/dist/spritesheet-375.png',
+ f'{root}/assets/dist/spritesheet-375.ron'])
+ writer.build(f'asset-375', 'phony', inputs=[f'{root}/assets/dist/spritesheet-375'])
+
+ writer.build(f'{root}/assets/dist/spritesheet-125', 'pack', inputs=files_125,
+ implicit_outputs=[f'{root}/assets/dist/spritesheet-125.png',
+ f'{root}/assets/dist/spritesheet-125.ron'])
+ writer.build(f'asset-125', 'phony', inputs=[f'{root}/assets/dist/spritesheet-125'])
+
+ writer.build(f'asset', 'phony',
+ inputs=[f'{root}/assets/dist/spritesheet-full', f'{root}/assets/dist/spritesheet-375',
+ f'{root}/assets/dist/spritesheet-125'])
+
+
+def generate_assets_build_command(root, assets, writer):
+ files_full = []
+ files_375 = []
+ files_125 = []
+ gen_inkscape(root, assets, writer, files_375, files_full, files_125)
+ gen_packers(root, writer, files_375, files_full, files_125)
+
+
+def main():
+ target = sys.argv[1]
+ env = sys.argv[2]
+ root = sys.argv[3]
+ print(f'[spacetime] Configuring target {target} with ENV={env}, buildroot={root}')
+
+ with open(f'{root}/build.ninja', 'w') as f:
+ writer = Writer(f)
+
+ writer.comment('Generated by spacetime.py')
+ writer.comment('Do not manually edit this file')
+
+ if env == 'prod' or target == 'asset':
+ assets = scan_assets(root)
+ generate_assets_build_command(root, assets, writer)
+
+
+
+ print(f'[spacetime] Configured build')
+
+
+if __name__ == "__main__":
+ main()
A st => st +114 -0
@@ 0,0 1,114 @@
+#!/bin/bash
+
+set -e
+
+SCRIPT_PATH=$(readlink -f "${BASH_SOURCE:-$0}")
+SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
+
+exec_spacetime() {
+ # args: target, environment, build root
+ echo "[*] Running configure command: 'python3 $SCRIPT_DIR/spacetime/spacetime.py $1 $2 $SCRIPT_DIR'"
+ python3 "$SCRIPT_DIR/spacetime/spacetime.py" "$1" "$2" "$SCRIPT_DIR"
+}
+
+exec_ninja() {
+ # args: target
+ echo "[*] Running build command: 'ninja -C $SCRIPT_DIR $1'"
+ ninja -C "$SCRIPT_DIR" "$1"
+}
+
+sub_help() {
+ echo "Spacetime - StarKingdoms build utility"
+ echo "Spacetime is a small utility program to generate Ninja build manifests for compiling StarKingdoms."
+ echo "Available targets:"
+ echo " help - Show this help screen" # done
+ echo " run_http - Compile the client and run a development http server for testing it"
+ echo " run_http_prod - Compile the client in production mode and run a development http server for testing it"
+ echo " run_server (default) - Compile and run the game server"
+ echo " build_client_bundle - Compile an optimized WebAssembly client bundle"
+ echo " build_client_bundle_prod - Compile an optimized WebAssembly client bundle using textures-fast"
+ echo " install_tooling - Install the compilation utilities required for compiling StarKingdoms" # done
+ echo " build_assets - Compile spritesheets in all three texture sizes for textures-fast" # done
+ echo " build_assets_full - Compile spritesheets in full size for textures-fast" # done
+ echo " build_assets_375 - Commpile 37.5% spritesheets for textures-fast" # done
+ echo " build_assets_125 - Compile 12.5% spritesheets for textures-fast" # done
+ echo " clean - Remove all generated files"
+}
+
+check_install_cargo() {
+ echo "[*] Checking for $1"
+ if ! command -v "$1" &> /dev/null
+ then
+ echo "[+] $1 was not found, installing via Cargo..."
+ cargo install "$2" $3
+ fi
+}
+
+check() {
+ echo "[*] Checking for $1"
+ if ! command -v "$1" &> /dev/null
+ then
+ echo "[x] $1 was not found but is required for the build process to continue. Install it with your system package manager, or, if supported, 'st install_tooling'"
+ exit 1
+ fi
+}
+
+check_all() {
+ check wasm-pack
+ check sheep
+ check inkscape
+}
+
+sub_install_tooling() {
+ check_install_cargo wasm-pack wasm-pack --no-default-features
+ check_install_cargo sheep sheep_cli
+ check inkscape
+ echo "[*] All required tools are installed"
+}
+
+sub_build_client_bundle() {
+ check_all
+ exec_spacetime client dev "$SCRIPT_DIR"
+ exec_ninja client
+}
+
+sub_build_assets() {
+ check_all
+ exec_spacetime asset dev "$SCRIPT_DIR"
+ exec_ninja asset
+}
+
+sub_build_assets_full() {
+ check_all
+ exec_spacetime asset dev "$SCRIPT_DIR"
+ exec_ninja asset-full
+}
+
+sub_build_assets_375() {
+ check_all
+ exec_spacetime asset dev "$SCRIPT_DIR"
+ exec_ninja asset-375
+}
+
+sub_build_assets_125() {
+ check_all
+ exec_spacetime asset dev "$SCRIPT_DIR"
+ exec_ninja asset-125
+}
+
+subcommand=$1
+case $subcommand in
+ "" | "-h" | "--help" | "help")
+ sub_help
+ ;;
+ *)
+ echo "[*] Running build command $subcommand"
+ shift
+ sub_${subcommand} $@
+ if [ $? = 127 ]; then
+ echo "Error: '$subcommand' is not a known subcommand." >&2
+ echo " Run 'st --help' for a list of known subcommands." >&2
+ exit 1
+ fi
+ ;;
+esac<
\ No newline at end of file