From cc60012f85e61e3a7e7692729add1ddf201a099a Mon Sep 17 00:00:00 2001 From: Thomas Touhey Date: Sat, 26 Dec 2020 12:18:19 +0100 Subject: [PATCH] some more edits --- .gitignore | 1 + Makefile | 2 + assets-fx/{sprites => img}/basic_turret.png | Bin assets-fx/{sprites => img}/enemy.png | Bin 5494 -> 5695 bytes assets-fx/{sprites => img}/zone_turret.png | Bin assets-fx/map/{map1.txt => basic.txt} | 0 converters.py | 129 ++- fxconv | 177 +++ fxconv.py | 1082 +++++++++++++++++++ src/game.c | 302 +++++- 10 files changed, 1682 insertions(+), 11 deletions(-) rename assets-fx/{sprites => img}/basic_turret.png (100%) rename assets-fx/{sprites => img}/enemy.png (72%) rename assets-fx/{sprites => img}/zone_turret.png (100%) rename assets-fx/map/{map1.txt => basic.txt} (100%) create mode 100755 fxconv create mode 100644 fxconv.py diff --git a/.gitignore b/.gitignore index 89cecb2..9b3b2cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build-fx *.g1a +__pycache__ diff --git a/Makefile b/Makefile index 370dbbb..49c45b2 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ # the [fxsdk] program. #--- +PATH := .:$(PATH) + # # Configuration # diff --git a/assets-fx/sprites/basic_turret.png b/assets-fx/img/basic_turret.png similarity index 100% rename from assets-fx/sprites/basic_turret.png rename to assets-fx/img/basic_turret.png diff --git a/assets-fx/sprites/enemy.png b/assets-fx/img/enemy.png similarity index 72% rename from assets-fx/sprites/enemy.png rename to assets-fx/img/enemy.png index 7501eb07c035610db66e10fab99fdd3c79b9a69f..6e7d1a13b1817c3b976fa59a77f9883fdb973685 100644 GIT binary patch delta 1273 zcmVUW(86X{e0-3^K=>5Ma4*88S-{4 zEcb@V-EYfw~_n=HmAH2+O+5V}JjCR6mX;d=LBW_hiUk zSNIX;RY)Rw-nAgNPmqcQM$eL=NN95`=ta(>A|W5;DZYv-yrXiC=#%+3P$cj30TcIR z=1YD!Px^84!xDzy)U~E$7FuR{Tk>(ZQ#}kh3ISfPhrFBT7z!x5e0b`o=go_RJW64oN zRYjAkW;Jy!TC?PoHRo)3UVhcYlBs1gb1PO|Jh^&ya}Qp;7A}IzQA;jXyp&QaoK{$_ z*j=Ge>wm(FENRgtFShtnmfBE0Ej4YqS@Y0JtDU=a+!H-_>)uPRE3GJ{HLbel)mC52 zS_evP$Vfv+9yWZGQ7>u~cC7E(L&=T8VYg#qNdNjelE6=_zh}k_!{vPml`}-6!PUbNhf= zpT9OOO4ta^*Kj5^3SK`bRd)R0H|cR1zPjO$q5rxeSzo5Yw{47jwT0*6c#+|`I9_CU zB95Ec@T+hijpku$zl-Kb_Vdv^l%q%8gs&gs8#(&Qp~Fu^^UEAP5Y0`7?=gBW!*g-G z$Qkfl9DlCb|Dhon_%Ed3m*Zbf?z&LzjV{`gVFMz6DRokc6o(A11s5~9RD~cY#8|=V zlqM+_lNgee;v4uT`Y;^?!6)zu6rBBUbZ{_>7Y_f&;hg`!_d?4~!n9o8g*2-Q*B^{7 zM_2ah8y$MI>9Q4sWj^q{6B^IwE0)ohzVT|F`}3?saT!V%sw~Wl3aovxGp};PDmHJ2 z{-OAPL2O597Kxw4J5d@LHoJbB%)%c}bDK__U7VY~Y8#hhPVulAGh<3ZMPHh!bA-d5 z)TJQco{~_S5UV%WOvR0;l#|PWXeiQc8uEH|o~M+tJf`w833sIOG(!ukj{o}qo+khS*vZ4EOMPCfI_H+}))`%q%GjlxR+01+#*lmz|) zBr{`WH8wXiWG!VhH8CwTW-&P}IW#giEoC%hGdDLkG&3}1W0STAkPA0BGBGtWGBY(c zHM3C&Oaqh63S1;HHZ)~pGchwQVl-noEi_~^HZ5T{G&wCYVKg^lF=8?|W->FgO$(+8 z1S-2c@00fwE=n2-7b6iS+57+i04hmDK~xyiV_={ZFd~@$|D#DUGB7YOFd++}E5T5} j$cU+k3ByiC1}Xypjc*1Bggz9p00000NkvXXu0mjf|3Xuv delta 1239 zcmV;|1StEzEcPmpPJh9Y5E9G5V9x0s^z!EiyIgT@T}kC-&a_D(N(dqJ$yCDj`&SSD z;NgXq5Y;^8lomWO$IJx-U-qZWo|ZWK{khH2RUgiY;Z`8!uuq5mO;4wTT@;M;g(I)W z#Cn~`@$m1j9330Uh=?(0hogKr65c21a8UMREh)RbJi@x|@_$&rAJvbe317p0{XH78 z*A)8*^Clz6Gv4N#9R?m??g-v($6vi$2@A#U;t((|hyK`?jC1#DtWl<50Jw%n=M%q+I5*gf^Ti z#x`3<^MLBDK!4trx~yoGGBL1piqxo3r)|ng6(L66*dUoJ%hb6TXRd=9fe;#XHp$Y& z!K?zWK+9(=H22MOyGo9Y8^MwZ&IWZ3qx*^Wp`+eM$rCMp<_dA~s4Rpr$c-LW07Cs% zM>-RHYpap-dJNaZCQC&(J<(FL<}J6f(S}mm;-*{LZ1XK| zrK8lk^w_nhZrywC<)lVo$NHu{PK|EVc#;~1c2dJqvzrSV*NF~hAjXkE+y(&@G!JHr zQ;Zx2y1{J8<4#jXlse!v8pJ>_Zp7N@WOpZb;eQrle2N?2$c2IKC&-0??gMgfxxGWJ zjbEF(NSK7iYdDg+2wp!J6?XjLH|cR1zPjMAqyM@i+3u?FZ6EzwZQ;2%?lL?V$6bae z;<&gCpN0EqG!IkzQ#41ipO5CD96h>C`1&Efk)y91I{ZX5pXTU+Xf85*kI`Eho{Qry z!x?jN{Jm=bhmNGHdtVmnH+Nc<$;jnc@l+4a(78vb~i+qC2C;@tFA+c=zXii^#NDH9SZy3$OYV|wgM z9SQ>ODG8+sv3fJjRNRP4IXN7Ph9cdjA#bbBbCoiZ$3$Kx;f_?ECJ*y}zt=v_>P@RN z&x0bUTMMezcnt54u;#=1Gpt@WWUc&uTfGI0RZ~e3=T': + if spawn_x is not None: + raise fxconv.FxconvError("multiple spawn points, please " + "choose, you dumbass") + spawn_x, spawn_y = car_no, line_no + grid_data += b'\1' + elif c == 'x': + if arr_x is not None: + raise fxconv.FxconvError("multiple castles, please " + "choose, you dumbass") + arr_x, arr_y = car_no, line_no + grid_data += b'\2' + else: + raise fxconv.FxconvError(f"invalid character at " + f"line {line_no + 1}, column {car_no + 1}") + + if spawn_x is None: + raise fxconv.FxconvError("no spawn point, is that some kind of " + "sick joke") + if arr_x is None: + raise fxconv.FxconvError("no castle, are you kidding me") + + def find_path(previous): + for delta_x, delta_y in ((0, -1), (1, 0), (0, 1), (-1, 0)): + x, y = previous[-1] + x += delta_x + y += delta_y + + if (x, y) == (arr_x, arr_y): + return previous + ((x, y),) + elif grid_data[y * w + x] != 1: + pass + elif (x, y) not in previous: + result = find_path(previous + ((x, y),)) + if result is not None: + return result + + return None + + path_to_arr = find_path(((spawn_x, spawn_y),)) + if path_to_arr is None: + raise fxconv.FxconvError(f"no path from spawn to castle") + + path_data = b'' + for (ax, ay), (bx, by) in zip(path_to_arr, path_to_arr[1:]): + path_data += int({ + (0, -1): 0, + (1, 0): 1, + (0, 1): 2, + (-1, 0): 3 + }[(bx - ax, by - ay)]).to_bytes(1, 'big') + + def round4len(x): + x = len(x) + result = (x // 4) * 4 + (x % 4 != 0) * 4 + return result + def round4data(x): + return x + b'\0' * (round4len(x) - len(x)) + + data = int(w).to_bytes(1, 'big') + int(h).to_bytes(1, 'big') + data += int(1).to_bytes(1, 'big') + int(1).to_bytes(1, 'big') + data += int(spawn_x).to_bytes(1, 'big') + data += int(spawn_y).to_bytes(1, 'big') + data += int(len(path_data)).to_bytes(1, 'big') + data += b'\0' * 9 + data += int(32).to_bytes(2, 'big') + data += int(32 + round4len(grid_data)).to_bytes(2, 'big') + data += b'\0' * 12 + data += round4data(grid_data) + data += round4data(path_data) - data = b'placeholder' fxconv.elf(data, output, "_" + params["name"], **target) # End of file. diff --git a/fxconv b/fxconv new file mode 100755 index 0000000..b3ef89f --- /dev/null +++ b/fxconv @@ -0,0 +1,177 @@ +#! /usr/bin/env python3 + +import getopt +import sys +import os +import fxconv +import subprocess + +# Note: this line is edited at compile time to insert the install folder +PREFIX="""\ +/usr +""".strip() + +help_string = f""" +usage: fxconv [-s] [files...] + fxconv -b -o [parameters...] + fxconv -i -o (--fx|--cg) [parameters...] + fxconv -f -o [parameters...] + +fxconv converts data files such as images and fonts into gint formats +optimized for fast execution, or into object files. + +Operating modes: + -s, --script Expose the fxconv module and run this Python script + -b, --binary Turn data into an object file, no conversion + -i, --image Convert to gint's bopti image format + -f, --font Convert to gint's topti font format + --libimg-image Convert to the libimg image format + +When using -s, additional arguments are stored in the [fxconv.args] variable of +the module. This is intended to be a restricted list of file names specified by +a Makefile, used to convert only a subset of the files in the script. + +The operating mode options are shortcuts to convert single files without a +script. They accept parameters with a "category.key:value" syntax, for example: + fxconv -f myfont.png -o myfont.o charset:ascii grid.padding:1 height:7 + +When converting images, use --fx (black-and-white calculators) or --cg (16-bit +color calculators) to specify the target machine. + +Install PREFIX is set to '{PREFIX}'. +""".strip() + +# Simple error-warnings system +def err(msg): + print("\x1b[31;1merror:\x1b[0m", msg, file=sys.stderr) +def warn(msg): + print("\x1b[33;1mwarning:\x1b[0m", msg, file=sys.stderr) + +# "converters" module from the user project +try: + import converters +except ImportError: + converters = None + +def main(): + # Default execution mode is to run a Python script for conversion + modes = "script binary image font bopti-image libimg-image" + mode = "s" + output = None + model = None + target = { 'toolchain': None, 'arch': None, 'section': None } + use_custom = False + + # Parse command-line arguments + + if len(sys.argv) == 1: + print(help_string, file=sys.stderr) + sys.exit(1) + + try: + longs = "help output= fx cg toolchain= arch= section= custom " + modes + opts, args = getopt.gnu_getopt(sys.argv[1:], "hsbifo:", longs.split()) + except getopt.GetoptError as error: + err(error) + sys.exit(1) + + for name, value in opts: + # Print usage + if name == "--help": + err(help_string, file=sys.stderr) + sys.exit(0) + # TODO: fxconv: verbose mode + elif name == "--verbose": + pass + elif name in [ "-o", "--output" ]: + output = value + elif name in [ "--fx", "--cg" ]: + model = name[2:] + elif name == "--toolchain": + target['toolchain'] = value + elif name == "--arch": + target['arch'] = value + elif name == "--section": + target['section'] = value + elif name == "--custom": + use_custom = True + mode = "custom" + # Other names are modes + else: + mode = name[1] if len(name)==2 else name[2:] + + # Remaining arguments + if args == []: + err(f"execution mode -{mode} expects an input file") + sys.exit(1) + input = args.pop(0) + + # In --script mode, run the Python script with an augmented PYTHONPATH + + if mode == "s": + if output is not None: + warn("option --output is ignored in script mode") + + if PREFIX == "": + err("unknown or invalid install path x_x") + sys.exit(1) + + env = os.environ.copy() + if "PYTHONPATH" in env: + env["PYTHONPATH"] += f":{PREFIX}/bin" + else: + env["PYTHONPATH"] = f"{PREFIX}/bin" + + p = subprocess.run([ sys.executable, input ], env=env) + if p.returncode != 0: + sys.exit(1) + + # In shortcut conversion modes, read parameters from the command-line + + else: + def check(arg): + if ':' not in arg: + warn(f"argument {arg} is not a valid parameter (ignored)") + return ':' in arg + + def insert(params, path, value): + if len(path) == 1: + params[path[0]] = value + return + if not path[0] in params: + params[path[0]] = {} + insert(params[path[0]], path[1:], value) + + args = [ arg.split(':', 1) for arg in args if check(arg) ] + params = {} + for (name, value) in args: + insert(params, name.split("."), value) + + if "type" in params: + pass + elif(len(mode) == 1): + params["type"] = { "b": "binary", "i": "image", "f": "font" }[mode] + else: + params["type"] = mode + + # Will be deprecated in the future + if params["type"] == "image": + warn("type 'image' is deprecated, use 'bopti-image' instead") + params["type"] = "bopti-image" + + # Use the custom module + custom = None + if use_custom: + if converters is None: + err("--custom specified but no [converters] module in wd") + sys.exit(1) + custom = converters.convert + + try: + fxconv.convert(input, params, target, output, model, custom) + except fxconv.FxconvError as e: + err(e) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/fxconv.py b/fxconv.py new file mode 100644 index 0000000..9f57349 --- /dev/null +++ b/fxconv.py @@ -0,0 +1,1082 @@ +""" +Convert data files into gint formats or object files +""" + +import os +import tempfile +import subprocess +import re + +from PIL import Image + +__all__ = [ + # Color names + "FX_BLACK", "FX_DARK", "FX_LIGHT", "FX_WHITE", "FX_ALPHA", + # Functions + "quantize", "convert", "elf", + # Reusable classes + "Area", "Grid", +] + +# +# Constants +# + +# Colors +FX_BLACK = ( 0, 0, 0, 255) +FX_DARK = ( 85, 85, 85, 255) +FX_LIGHT = (170, 170, 170, 255) +FX_WHITE = (255, 255, 255, 255) +FX_ALPHA = ( 0, 0, 0, 0) + +# fx-9860G profiles +class FxProfile: + def __init__(self, id, name, colors, layers): + """ + Construct an FxProfile object. + * [id] is the profile ID in bopti + * [name] is the profile's name as seen in the "profile" key + * [colors] is the set of supported colors + * [layers] is a list of layer functions + """ + + self.id = id + self.name = name + self.gray = FX_LIGHT in colors or FX_DARK in colors + self.colors = colors + self.layers = layers + + @staticmethod + def find(name): + """Find a profile by name.""" + for profile in FX_PROFILES: + if profile.name == name: + return profile + return None + +FX_PROFILES = [ + # Usual black-and-white bitmaps without transparency, as in MonochromeLib + FxProfile(0x0, "mono", { FX_BLACK, FX_WHITE }, [ + lambda c: (c == FX_BLACK), + ]), + # Black-and-white with transparency, equivalent of two bitmaps in ML + FxProfile(0x1, "mono_alpha", { FX_BLACK, FX_WHITE, FX_ALPHA }, [ + lambda c: (c != FX_ALPHA), + lambda c: (c == FX_BLACK), + ]), + # Gray engine bitmaps, reference could have been Eiyeron's Gray Lib + FxProfile(0x2, "gray", { FX_BLACK, FX_DARK, FX_LIGHT, FX_WHITE }, [ + lambda c: (c in [FX_BLACK, FX_LIGHT]), + lambda c: (c in [FX_BLACK, FX_DARK]), + ]), + # Gray images with transparency, unfortunately 3 layers since 5 colors + FxProfile(0x3, "gray_alpha", + { FX_BLACK, FX_DARK, FX_LIGHT, FX_WHITE, FX_ALPHA }, [ + lambda c: (c != FX_ALPHA), + lambda c: (c in [FX_BLACK, FX_LIGHT]), + lambda c: (c in [FX_BLACK, FX_DARK]), + ]), +] + +# fx-CG 50 profiles +class CgProfile: + def __init__(self, id, name, alpha): + """ + Construct a CgProfile object. + * [id] is the profile ID in bopti + * [name] is the profile name as found in the specification key + * [alpha] is True if this profile supports alpha, False otherwise + """ + + self.id = id + self.name = name + self.supports_alpha = alpha + + @staticmethod + def find(name): + """Find a profile by name.""" + for profile in CG_PROFILES: + if profile.name == name: + return profile + return None + +CG_PROFILES = [ + # 16-bit R5G6B5 + CgProfile(0x0, "r5g6b5", False), + # 16-bit R5G6B5 with alpha + CgProfile(0x1, "r5g6b5a", True), + # 8-bit palette + CgProfile(0x2, "p8", True), + # 4-bit palette + CgProfile(0x3, "p4", True), +] + +# Libimg flags +LIBIMG_FLAG_OWN = 1 +LIBIMG_FLAG_RO = 2 + +# +# Character sets +# + +FX_CHARSETS = { + # Digits 0...9 + "numeric": [ (ord('0'), 10) ], + # Uppercase letters A...Z + "upper": [ (ord('A'), 26) ], + # Upper and lowercase letters A..Z, a..z + "alpha": [ (ord('A'), 26), (ord('a'), 26) ], + # Letters and digits A..Z, a..z, 0..9 + "alnum": [ (ord('A'), 26), (ord('a'), 26), (ord('0'), 10) ], + # All printable characters from 0x20 to 0x7e + "print": [ (0x20, 95) ], + # All 128 ASII characters + "ascii": [ (0x00, 128) ], + # Custom Unicode block intervals + "unicode": [], +} + +# +# Area specifications +# + +class Area: + """ + A subrectangle of an image, typically used for pre-conversion cropping. + """ + + def __init__(self, area, img): + """ + Construct an Area object from a dict specification. The following keys + may be used to specific the position and size of the rectangle: + + * "x", "y" (int strings, default to 0) + * "width", "height" (int strings, default to image dimensions) + * "size" ("WxH" where W and H are the width and height) + + The Area objects has attributes "x", "y", "w" and "h". Both positions + default to 0 and both sizes to the corresponding image dimensions. + """ + + self.x = int(area.get("x", 0)) + self.y = int(area.get("y", 0)) + self.w = int(area.get("width", img.width)) + self.h = int(area.get("height", img.height)) + + if "size" in area: + self.w, self.h = map(int, area["size"].split("x")) + + def tuple(self): + """Return the tuple representation (x,y,w,h), suitable for .crop(). """ + return (self.x, self.y, self.w, self.h) + +# +# Grid specifications +# + +class Grid: + """ + A grid over an image, used to isolate glyphs in fonts and tiles in maps. + Supports several types of spacing. To apply an outer border, use crop + through an Area before using the Grid. + """ + + def __init__(self, grid): + """ + Construct a Grid object from a dict specification. The following keys + may be used to specify the dimension and spacing of the cells: + + * "border" (int string, defaults to 0) + * "padding" (int string, defaults to 0) + * "width", "height" (int strings, mandatory if "size" not set) + * "size" ("WxH" where W and H are the cell width/height) + + The Grid object has attributes "border", "padding", "w" and "h". Each + cell is of size "(w,h)" and has "padding" pixels of proper padding + around it. Additionally, cells are separated by a border of size + "border"; this includes an outer border. + """ + + self.border = int(grid.get("border", 0)) + self.padding = int(grid.get("padding", 0)) + + self.w = int(grid.get("width", -1)) + self.h = int(grid.get("height", -1)) + + if "size" in grid: + self.w, self.h = map(int, grid["size"].split("x")) + + if self.w <= 0 or self.h <= 0: + raise FxconvError("size of grid unspecified or invalid") + + def size(self, img): + """Count the number of elements in the grid.""" + b, p, w, h = self.border, self.padding, self.w, self.h + + # Padding-extended parameters + W = w + 2 * p + H = h + 2 * p + + columns = (img.width - b) // (W + b) + rows = (img.height - b) // (H + b) + return columns * rows + + + def iter(self, img): + """Yields subrectangles of the grid as tuples (x,y,w,h).""" + b, p, w, h = self.border, self.padding, self.w, self.h + + # Padding-extended parameters + W = w + 2 * p + H = h + 2 * p + + columns = (img.width - b) // (W + b) + rows = (img.height - b) // (H + b) + + for r in range(rows): + for c in range(columns): + x = b + c * (W + b) + p + y = b + r * (H + b) + p + yield (x, y, x + w, y + h) + +# +# Helpers +# + +def _encode_word(x): + return bytes([ (x >> 8) & 255, x & 255 ]) +def _encode_long(x): + return bytes([ (x >> 24) & 255, (x >> 16) & 255, (x >> 8) & 255, x & 255 ]) + +# +# Binary conversion +# + +def convert_binary(input, output, params, target): + data = open(input, "rb").read() + elf(data, output, "_" + params["name"], **target) + +# +# Image conversion for fx-9860G +# + +def convert_bopti_fx(input, output, params, target): + if isinstance(input, Image.Image): + img = input.copy() + else: + img = Image.open(input) + if img.width >= 4096 or img.height >= 4096: + raise FxconvError(f"'{input}' is too large (max. 4095x4095)") + + # Expand area.size and get the defaults. Crop image to resulting area. + area = Area(params.get("area", {}), img) + img = img.crop(area.tuple()) + + # Quantize the image and check the profile + img = quantize(img, dither=False) + + # If profile is provided, check its validity, otherwise use the smallest + # compatible profile + + colors = { y for (x,y) in img.getcolors() } + + if "profile" in params: + name = params["profile"] + p = FxProfile.find(name) + + if p is None: + raise FxconvError(f"unknown profile {name} in '{input}'") + if colors - p.colors: + raise FxconvError(f"{name} has too few colors for '{input}'") + else: + name = "gray" if FX_LIGHT in colors or FX_DARK in colors else "mono" + if FX_ALPHA in colors: name += "_alpha" + p = FxProfile.find(name) + + # Make the image header + + header = bytes ([(0x80 if p.gray else 0) + p.id]) + encode24bit = lambda x: bytes([ x >> 16, (x & 0xff00) >> 8, x & 0xff ]) + header += encode24bit((img.size[0] << 12) + img.size[1]) + + # Split the image into layers depending on the profile and zip them all + + layers = [ _image_project(img, layer) for layer in p.layers ] + count = len(layers) + size = len(layers[0]) + + data = bytearray(count * size) + n = 0 + + for longword in range(size // 4): + for layer in layers: + for i in range(4): + data[n] = layer[4 * longword + i] + n += 1 + + # Generate the object file + + elf(header + data, output, "_" + params["name"], **target) + +def _image_project(img, f): + # New width and height + w = (img.size[0] + 31) // 32 + h = (img.size[1]) + + data = bytearray(4 * w * h) + im = img.load() + + # Now generate a 32-bit byte sequence + for y in range(img.size[1]): + for x in range(img.size[0]): + bit = int(f(im[x, y])) + data[4 * y * w + (x >> 3)] |= (bit << (~x & 7)) + + return data + +# +# Image conversion for fx-CG 50 +# + +def convert_bopti_cg(input, output, params, target): + if isinstance(input, Image.Image): + img = input.copy() + else: + img = Image.open(input) + if img.width >= 65536 or img.height >= 65536: + raise FxconvError(f"'{input}' is too large (max. 65535x65535)") + + # Crop image to key "area" + area = Area(params.get("area", {}), img) + img = img.crop(area.tuple()) + + # If no profile is specified, fall back to r5g6b5 or r5g6b5a later on + name = params.get("profile", None) + if name is not None: + profile = CgProfile.find(name) + + if name in [ "r5g6b5", "r5g6b5a", None ]: + # Encode the image into the 16-bit format + encoded, alpha = r5g6b5(img) + + name = "r5g6b5" if alpha is None else "r5g6b5a" + profile = CgProfile.find(name) + + elif name in [ "p4", "p8" ]: + # Encoded the image into 16-bit with a palette of 16 or 256 entries + color_count = 1 << int(name[1]) + encoded, palette, alpha = r5g6b5(img, color_count=color_count) + + encoded = palette + encoded + + else: + raise FxconvError(f"unknown color profile '{name}'") + + if alpha is not None and not profile.supports_alpha: + raise FxconvError(f"'{input}' has transparency; use r5g6b5a, p8 or p4") + + w, h, a = img.width, img.height, alpha or 0x0000 + + header = bytearray([ + 0x00, profile.id, # Profile identification + a >> 8, a & 0xff, # Alpha color + w >> 8, w & 0xff, # Width + h >> 8, h & 0xff, # Height + ]) + + elf(header + encoded, output, "_" + params["name"], **target) + +# +# Font conversion +# + +def _trim(img): + def blank(x): + return all(px[x,y] == FX_WHITE for y in range(img.height)) + + left = 0 + right = img.width + px = img.load() + + while left + 1 < right and blank(left): + left += 1 + while right - 1 > left and blank(right - 1): + right -= 1 + + return img.crop((left, 0, right, img.height)) + +def _blockstart(name): + m = re.match(r'(?:U\+)?([0-9A-Fa-f]+)\.', name) + + if m is None: + return None + try: + return int(m[1], base=16) + except Exception as e: + return None + +def convert_topti(input, output, params, target): + + #-- + # Character set + #-- + + if "charset" not in params: + raise FxconvError("'charset' attribute is required and missing") + + charset = params["charset"] + blocks = FX_CHARSETS.get(charset, None) + if blocks is None: + raise FxconvError(f"unknown character set '{charset}'") + + # Will be recomputed later for Unicode fonts + glyph_count = sum(length for start, length in blocks) + + #-- + # Image input + #-- + + grid = Grid(params.get("grid", {})) + + # When using predefined charsets with a single image, apply the area and + # check that the number of glyphs is correct + if charset != "unicode": + if isinstance(input, Image.Image): + img = input.copy() + else: + img = Image.open(input) + area = Area(params.get("area", {}), img) + img = img.crop(area.tuple()) + + # Quantize it (only black pixels will be encoded into glyphs) + img = quantize(img, dither=False) + + if glyph_count > grid.size(img): + raise FxconvError( + f"not enough elements in grid (got {grid.size(img)}, "+ + f"need {glyph_count} for '{charset}')") + + inputs = [ img ] + + # In Unicode mode, load images for the provided directory, but don't apply + # the area (this makes no sense since the sizes are different) + else: + try: + files = os.listdir(input) + except Exception as e: + raise FxconvError( + f"cannot scan directory '{input}' to discover blocks for the"+ + f"unicode charset: {str(e)}") + + # Keep only files with basenames like "" or "U+" and sort + # them by code point order (for consistency) + files = [e for e in files if _blockstart(e) is not None] + files = sorted(files, key=_blockstart) + + # Open all images and guess the block size + inputs = [] + for file in files: + img = Image.open(os.path.join(input, file)) + img = quantize(img, dither=False) + inputs.append(img) + + blocks = [(_blockstart(e), grid.size(img)) + for e, img in zip(files, inputs)] + + # Recompute the total glyph count + glyph_count = sum(length for start, length in blocks) + + + #-- + # Proportionality and metadata + #-- + + proportional = (params.get("proportional", "false") == "true") + title = bytes(params.get("title", ""), "utf-8") + bytes([0]) + + flags = set(params.get("flags", "").split(",")) + flags.remove("") + flags_std = { "bold", "italic", "serif", "mono" } + + if flags - flags_std: + raise FxconvError(f"unknown flags: {', '.join(flags - flags_std)}") + + bold = int("bold" in flags) + italic = int("italic" in flags) + serif = int("serif" in flags) + mono = int("mono" in flags) + + flags = (bold << 7) | (italic << 6) | (serif << 5) | (mono << 4) \ + | int(proportional) + # Default line height to glyph height + line_height = params.get("height", grid.h) + + # Default character spacing to 1 + char_spacing = params.get("char-spacing", 1) + + #-- + # Encoding blocks + #--- + + def encode_block(b): + start, length = b + return _encode_long((start << 12) | length) + + data_blocks = b''.join(encode_block(b) for b in blocks) + + #-- + # Encoding glyphs + #-- + + data_glyphs = [] + total_glyphs = 0 + data_width = bytearray() + data_index = bytearray() + + for img in inputs: + for (number, region) in enumerate(grid.iter(img)): + # Upate index + if not (number % 8): + idx = total_glyphs // 4 + data_index += _encode_word(idx) + + # Get glyph area + glyph = img.crop(region) + if proportional: + glyph = _trim(glyph) + data_width.append(glyph.width) + + length = 4 * ((glyph.width * glyph.height + 31) >> 5) + bits = bytearray(length) + offset = 0 + px = glyph.load() + + for y in range(glyph.size[1]): + for x in range(glyph.size[0]): + color = (px[x,y] == FX_BLACK) + bits[offset >> 3] |= ((color * 0x80) >> (offset & 7)) + offset += 1 + + data_glyphs.append(bits) + total_glyphs += length + + data_glyphs = b''.join(data_glyphs) + + #--- + # Object file generation + #--- + + # In the data section, first put the raw data and blocks (4-aligned), then + # the index (2-aligned), then the glyph size array and font name + # (1-aligned). This avoids any additional alingment/padding issues. + if proportional: + data = data_glyphs + data_blocks + data_index + data_width + title + off_blocks = len(data_glyphs) + off_index = off_blocks + len(data_blocks) + off_width = off_index + len(data_index) + off_title = off_width + len(data_width) + + assembly2 = f""" + .long _{params["name"]}_data + {off_index} + .long _{params["name"]}_data + {off_width} + """ + # For fixed-width fonts, just put the glyph data and tht font title + else: + data = data_glyphs + data_blocks + title + off_blocks = len(data_glyphs) + off_title = off_blocks + len(data_blocks) + + assembly2 = f""" + .word {grid.w} + .word {(grid.w * grid.h + 31) >> 5} + """ + + # Make the title pointer NUL if no title is specified + if len(title) > 1: + ref_title = f'_{params["name"]}_data + {off_title}' + else: + ref_title = '0' + + # Header followed by the proportional of fixed subheader + assembly = f""" + .section .rodata + .global _{params["name"]} + + _{params["name"]}: + .long {ref_title} + .byte {flags} + .byte {line_height} + .byte {grid.h} + .byte {len(blocks)} + .long {glyph_count} + .byte {char_spacing} + .zero 3 + .long _{params["name"]}_data + {off_blocks} + .long _{params["name"]}_data + """ + assembly2 + + dataname = "_{}_data".format(params["name"]) + elf(data, output, dataname, assembly=assembly, **target) + +# +# libimg conversion for fx-9860G +# + +def convert_libimg_fx(input, output, params, target): + if isinstance(input, Image.Image): + img = input.copy() + else: + img = Image.open(input) + if img.width >= 65536 or img.height >= 65536: + raise FxconvError(f"'{input}' is too large (max. 65535x65535)") + + # Crop image to area + area = Area(params.get("area", {}), img) + img = img.crop(area.tuple()) + + # Quantize the image. We don't need to check if there is gray; the VRAM + # rendering function for mono output will adjust at runetime + img = quantize(img, dither=False) + code = { FX_WHITE: 0, FX_LIGHT: 1, FX_DARK: 2, FX_BLACK: 3, FX_ALPHA: 4 } + + # Encode image as a plain series of pixels + data = bytearray(img.width * img.height) + im = img.load() + i = 0 + + for y in range(img.height): + for x in range(img.width): + data[i] = code[im[x, y]] + i += 1 + + assembly = f""" + .section .rodata + .global _{params["name"]} + + _{params["name"]}: + .word {img.width} + .word {img.height} + .word {img.width} + .byte {LIBIMG_FLAG_RO} + .byte 0 + .long _{params["name"]}_data + """ + + dataname = "_{}_data".format(params["name"]) + elf(data, output, dataname, assembly=assembly, **target) + + +# +# libimg conversion for fx-CG 50 +# + +def convert_libimg_cg(input, output, params, target): + if isinstance(input, Image.Image): + img = input.copy() + else: + img = Image.open(input) + if img.width >= 65536 or img.height >= 65536: + raise FxconvError(f"'{input}' is too large (max. 65535x65535)") + + # Crop image to key "area" + area = Area(params.get("area", {}), img) + img = img.crop(area.tuple()) + + # Encode the image into 16-bit format and force the alpha to 0x0001 + encoded, alpha = r5g6b5(img, alpha=(0x0001,0x0000)) + + assembly = f""" + .section .rodata + .global _{params["name"]} + + _{params["name"]}: + .word {img.width} + .word {img.height} + .word {img.width} + .byte {LIBIMG_FLAG_RO} + .byte 0 + .long _{params["name"]}_data + """ + + dataname = "_{}_data".format(params["name"]) + elf(encoded, output, dataname, assembly=assembly, **target) + +# +# Exceptions +# + +class FxconvError(Exception): + pass + +# +# API +# + +def quantize(img, dither=False): + """ + Convert a PIL.Image.Image into an RGBA image with only these colors: + * FX_BLACK = ( 0, 0, 0, 255) + * FX_DARK = ( 85, 85, 85, 255) + * FX_LIGHT = (170, 170, 170, 255) + * FX_WHITE = (255, 255, 255, 255) + * FX_ALPHA = ( 0, 0, 0, 0) + + The alpha channel is first flattened to either opaque of full transparent, + then all colors are quantized into the 4-shade scale. Floyd-Steinberg + dithering can be used, although most applications will prefer nearest- + neighbor coloring. + + Arguments: + img -- Input image, in any format + dither -- Enable Floyd-Steinberg dithering [default: False] + + Returns a quantized PIL.Image.Image. + """ + + # Our palette will have only 4 colors for the gray engine + colors = [ FX_BLACK, FX_DARK, FX_LIGHT, FX_WHITE ] + + # Create the palette + palette = Image.new("RGBA", (len(colors), 1)) + for (i, c) in enumerate(colors): + palette.putpixel((i, 0), c) + palette = palette.convert("P") + + # Make the image RGBA in case it was indexed so that transparent pixels are + # represented in an alpha channel + if img.mode == "P": + img = img.convert("RGBA") + + # Save the alpha channel, and make it either full transparent or opaque + try: + alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE) + except: + alpha_channel = Image.new("L", img.size, 255) + + # Apply the palette to the original image (transparency removed) + img = img.convert("RGB") + + # Let's do an equivalent of the following, but with a dithering setting: + # img = img.quantize(palette=palette) + + img.load() + palette.load() + im = img.im.convert("P", int(dither), palette.im) + img = img._new(im).convert("RGB") + + # Put back the alpha channel + img.putalpha(alpha_channel) + + # Premultiply alpha + pixels = img.load() + for y in range(img.size[1]): + for x in range(img.size[0]): + r, g, b, a = pixels[x, y] + if a == 0: + r, g, b, = 0, 0, 0 + pixels[x, y] = (r, g, b, a) + + return img + +def r5g6b5(img, color_count=0, alpha=None): + """ + Convert a PIL.Image.Image into an R5G6B5 byte stream. If there are + transparent pixels, chooses a color to implement alpha and replaces them + with this color. + + Returns the converted image as a bytearray and the alpha value, or None if + no alpha value was used. + + If color_count is provided, it should be either 16 or 256. The image is + encoded with a palette of this size. Returns the converted image as a + bytearray, the palette as a bytearray, and the alpha value (None if there + were no transparent pixels). + + If alpha is provided, it should be a pair (alpha value, replacement). + Trandarpent pixels will be encoded with the specified alpha value and + pixels with the value will be encoded with the replacement. + """ + + def rgb24to16(r, g, b): + r = (r & 0xff) >> 3 + g = (g & 0xff) >> 2 + b = (b & 0xff) >> 3 + return (r << 11) | (g << 5) | b + + # Save the alpha channel and make it 1-bit + try: + alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE) + alpha_levels = { t[1]: t[0] for t in alpha_channel.getcolors() } + has_alpha = 0 in alpha_levels + replacement = None + + if has_alpha: + alpha_pixels = alpha_channel.load() + + except ValueError: + has_alpha = False + + # Convert the input image to RGB + img = img.convert("RGB") + + # Optionally convert to palette + if color_count: + palette_size = color_count - int(has_alpha) + img = img.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE, + colors=palette_size) + palette = img.getpalette() + + pixels = img.load() + + # Choose an alpha color + + if alpha is not None: + alpha, replacement = alpha + + elif color_count > 0: + # Transparency is mapped to the last palette element, if there are no + # transparent pixels then select an index out of bounds. + alpha = color_count - 1 if has_alpha else 0xffff + + elif has_alpha: + # Compute the set of all used R5G6B5 colors + colormap = set() + + for y in range(img.height): + for x in range(img.width): + if alpha_pixels[x, y] > 0: + colormap.add(rgb24to16(*pixels[x, y])) + + # Choose an alpha color among the unused ones + available = set(range(65536)) - colormap + + if not available: + raise FxconvError("image uses all 65536 colors and alpha") + alpha = available.pop() + + else: + alpha = None + + def alpha_encoding(color, a): + if a > 0: + if color == alpha: + return replacement + else: + return color + else: + return alpha + + # Create a byte array with all encoded pixels + + pixel_count = img.width * img.height + + if not color_count: + size = pixel_count * 2 + elif color_count == 256: + size = pixel_count + elif color_count == 16: + size = (pixel_count + 1) // 2 + + # Result of encoding + encoded = bytearray(size) + # Number of pixels encoded so far + entries = 0 + # Offset into the array + offset = 0 + + for y in range(img.height): + for x in range(img.width): + a = alpha_pixels[x, y] if has_alpha else 0xff + + if not color_count: + c = alpha_encoding(rgb24to16(*pixels[x, y]), a) + encoded[offset] = c >> 8 + encoded[offset+1] = c & 0xff + offset += 2 + + elif color_count == 16: + c = alpha_encoding(pixels[x, y], a) + + # Aligned pixels: left 4 bits = high 4 bits of current byte + if (entries % 2) == 0: + encoded[offset] |= (c << 4) + # Unaligned pixels: right 4 bits of current byte + else: + encoded[offset] |= c + offset += 1 + + elif color_count == 256: + c = alpha_encoding(pixels[x, y], a) + encoded[offset] = c + offset += 1 + + entries += 1 + + if not color_count: + return encoded, alpha + + # Encode the palette as R5G6B5 + + encoded_palette = bytearray(2 * color_count) + + for c in range(color_count - int(has_alpha)): + r, g, b = palette[3*c], palette[3*c+1], palette[3*c+2] + rgb16 = rgb24to16(r, g, b) + + encoded_palette[2*c] = rgb16 >> 8 + encoded_palette[2*c+1] = rgb16 & 0xff + + return encoded, encoded_palette, alpha + +def convert(input, params, target, output=None, model=None, custom=None): + """ + Convert a data file into an object that exports the following symbols: + * _ + * __end + * __size + The variable name is obtained from the parameter dictionary . + + Arguments: + input -- Input file path + params -- Parameter dictionary + target -- String dictionary keys 'toolchain', 'arch' and 'section' + output -- Output file name [default: with suffix '.o'] + model -- 'fx' or 'cg' (some conversions require this) [default: None] + custom -- Custom conversion function + + Produces an output file and returns nothing. + """ + + if output is None: + output = os.path.splitext(input)[0] + ".o" + + if "name" not in params: + raise FxconvError(f"no name specified for conversion '{input}'") + + if target["arch"] is None: + target["arch"] = model + + if "type" not in params: + raise FxconvError(f"missing type in conversion '{input}'") + elif params["type"] == "binary": + convert_binary(input, output, params, target) + elif params["type"] == "bopti-image" and model in [ "fx", None ]: + convert_bopti_fx(input, output, params, target) + elif params["type"] == "bopti-image" and model == "cg": + convert_bopti_cg(input, output, params, target) + elif params["type"] == "font": + convert_topti(input, output, params, target) + elif params["type"] == "libimg-image" and model in [ "fx", None ]: + convert_libimg_fx(input, output, params, target) + elif params["type"] == "libimg-image" and model == "cg": + convert_libimg_cg(input, output, params, target) + elif custom is not None: + custom(input, output, params, target) + else: + raise FxconvError(f'unknown resource type \'{params["type"]}\'') + +def elf(data, output, symbol, toolchain=None, arch=None, section=None, + assembly=None): + """ + Call objcopy to create an object file from the specified data. The object + file will export three symbols: + * + * _end + * _size + + The symbol name must have a leading underscore if it is to be declared and + used from a C program. + + The toolchain can be any target triplet for which the compiler is + available. The architecture is deduced from some typical triplets; + otherwise it can be set, usually as "sh3" or "sh4-nofpu". This affects the + --binary-architecture flag of objcopy. If arch is set to "fx" or "cg", this + function tries to be smart and: + + * Uses the name of the compiler if it contains a full architecture name + such as "sh3", "sh4" or "sh4-nofpu"; + * Uses "sh3" for fx9860g and "sh4-nofpu" for fxcg50 if the toolchain is + "sh-elf", which is a custom set; + * Fails otherwise. + + The section name can be specified, along with its flags. A typical example + would be section=".rodata,contents,alloc,load,readonly,data", which is the + default. + + If assembly is set to a non-empty assembly program, this function also + generates a temporary ELF file by assembling this piece of code, and merges + it into the original one. + + Arguments: + data -- A bytes-like object with data to embed into the object file + output -- Name of output file + symbol -- Chosen symbol name + toolchain -- Target triplet [default: "sh3eb-elf"] + arch -- Target architecture [default: try to guess] + section -- Target section [default: above variation of .rodata] + assembly -- Additional assembly code [default: None] + + Produces an output file and returns nothing. + """ + + if toolchain is None: + toolchain = "sh3eb-elf" + if section is None: + section = ".rodata,contents,alloc,load,readonly,data" + + if arch in ["fx", "cg", None] and toolchain in ["sh3eb-elf", "sh4eb-elf", + "sh4eb-nofpu-elf"]: + arch = toolchain.replace("eb-", "-")[:-4] + + elif arch == "fx" and toolchain == "sh-elf": + arch = "sh3" + elif arch == "cg" and toolchain == "sh-elf": + arch = "sh4-nofpu" + + elif arch in ["fx", "cg", None]: + raise FxconvError(f"non-trivial architecture for {toolchain} must be "+ + "specified") + + fp_obj = tempfile.NamedTemporaryFile() + fp_obj.write(data) + fp_obj.flush() + + if assembly is not None: + fp_asm = tempfile.NamedTemporaryFile() + fp_asm.write(assembly.encode('utf-8')) + fp_asm.flush() + + proc = subprocess.run([ + f"{toolchain}-as", "-c", fp_asm.name, "-o", fp_asm.name + ".o" ]) + if proc.returncode != 0: + raise FxconvError(f"as returned {proc.returncode}") + + sybl = "_binary_" + fp_obj.name.replace("/", "_") + + objcopy_args = [ + f"{toolchain}-objcopy", "-I", "binary", "-O", "elf32-sh", + "--binary-architecture", arch, "--file-alignment", "4", + "--rename-section", f".data={section}", + "--redefine-sym", f"{sybl}_start={symbol}", + "--redefine-sym", f"{sybl}_end={symbol}_end", + "--redefine-sym", f"{sybl}_size={symbol}_size", + fp_obj.name, output if assembly is None else fp_obj.name + "-tmp" ] + + proc = subprocess.run(objcopy_args) + if proc.returncode != 0: + raise FxconvError(f"objcopy returned {proc.returncode}") + + if assembly is not None: + proc = subprocess.run([ + f"{toolchain}-ld", "-r", fp_obj.name + "-tmp", fp_asm.name + ".o", + "-o", output ]) + + os.remove(fp_obj.name + "-tmp") + os.remove(fp_asm.name + ".o") + + if proc.returncode != 0: + raise FxconvError("ld returned {proc.returncode}") + + fp_asm.close() + + fp_obj.close() diff --git a/src/game.c b/src/game.c index 7eb7694..038a5b2 100644 --- a/src/game.c +++ b/src/game.c @@ -1,25 +1,313 @@ #include "main.h" #include #include +#define NENEMIES 64 -/* Map definition. - * This defines how we'll naviguate through the map. */ +/* State definition, as used throughout the main game. + * + * For each enemy, the data is: + * - alive: if the enemy is alive or dead. + * + * - nstepf: number of frames for one step, defines the speed of the + * enemy. + * + * - x, y: calculated only for display and collision. + * - step, substep: where the AI is at. `step` is the index starting at zero + * of the path where is at, `substep` is the substep from 0 to 7, looping + * when 8 (augmenting step, except when at the castle). + * - nfwait: number of frames to be waited. + * - pdr: the enemy that spawned previously or is the next further on the + * path. Simple chain list that could evolve later if some enemies are + * slowed down or have various speeds, allowing some enemies to overtake + * others. */ -void dmap(void *map) +struct enemy { + int alive; + + int nstepf; + + int x, y; + int step, substep; + int nfwait; + struct enemy **ndrp; /* pointer on the pointer to this enemy structure */ + struct enemy *pdr; /* previous enemy to be drawn */ +}; + +struct state { + /* Map information: + * + * - w, h: dimensions + * - sp_x, sp_y: enemy spawn coordinates + * - nep: number of enemy path */ + + int w, h; + int sp_x, sp_y; + int nep; /* number of enemies path. */ + uint8_t *grid; + uint8_t *ep; + + /* AI information: + * + * - tbsp: time between enemy spawns. + * - tnsp: time to next spawn (as a number of ticks). + * - nae: number of currently alive enemies. + * - first_enemy: pointer to the last enemy to have spawned, + * to draw first */ + + int tbsp, tnsp; + int nae; + struct enemy *first_enemy; + + /* Camera, cursor and display information: + * + * - cam_x, cam_y: camera coordinates, always displays 16x8 tiles + * - cur_x, cur_y: map coordinate for the cursor. + * - drf: draw frame. */ + + int cam_x, cam_y; + int cur_x, cur_y; + int drf; /* draw frame, varies from 0 to 255 and looping */ + + /* Raw data */ + + struct enemy enemies[NENEMIES]; +}; + +static struct state state; + +/* init(raw): initialize the state with the given raw map data. */ + +static void init(uint8_t *raw) { + int i; + /* Initialize map information */ + + state.w = raw[0]; + state.h = raw[1]; + state.sp_x = raw[4]; + state.sp_y = raw[5]; + state.nep = raw[6]; + state.grid = &raw[(raw[16] << 8) | raw[17]]; + state.ep = &raw[(raw[18] << 8) | raw[19]]; + + /* Initialize AI information */ + + state.tbsp = 50; + state.tnsp = state.tbsp; + state.nae = 0; + state.first_enemy = NULL; + + for (i = 0; i < NENEMIES; i++) + state.enemies[i].alive = 0; + + /* Initialize display information */ + + state.cam_x = raw[2]; + state.cam_y = raw[3]; + state.cur_x = 1; + state.cur_y = 1; + state.drf = 0; +} + +/* tick(): tick the game engine. */ + +static void tick() +{ + struct enemy *ep; + int i; + + for (ep = state.first_enemy; ep; ep = ep->pdr) { + if (!--ep->nfwait) { + ep->nfwait = ep->nstepf; + + switch (state.ep[ep->step]) { + case 0: + ep->y--; + break; + case 1: + ep->x++; + break; + case 2: + ep->y++; + break; + case 3: + ep->x--; + break; + } + + ep->substep++; + if (ep->substep == 8) { + ep->step++; + ep->substep = 0; + + if (ep->step == state.nep) { + ep->alive = 0; + if (ep->ndrp) + (*ep->ndrp) = ep->pdr; + + state.nae--; + + /* TODO: remove one life to the castle */ + } + } + } + } + + /* Spawn enemies if required */ + + if (!--state.tnsp) { + state.tnsp = state.tbsp; + + if (state.nae < NENEMIES) { + for (i = 0, ep = &state.enemies[0]; i < NENEMIES; i++, ep++); + ep->pdr = state.first_enemy; + ep->pdr->ndrp = &ep->pdr; + state.first_enemy = ep; + ep->ndrp = &state.first_enemy; + + ep->alive = 1; + ep->x = state.sp_x << 3; + ep->y = state.sp_y << 3; + ep->step = 0; + ep->substep = 0; + + /* TODO: use a given type of enemy? */ + + ep->nstepf = 10; + ep->nfwait = ep->nstepf; + + state.nae++; + } + } +} + +/* tile_cam(x, y): return the type of the tile at coordinates (x, y) relative + * to the current camera position. + * + * If out of bounds, a basic placement tile (id. 0) will be returned */ + +static inline int tile_cam(int x, int y) +{ + x += state.cam_x; + y += state.cam_y; + + if (x < 0 || x >= state.w || y < 0 || y >= state.h) + return 0; + return state.grid[y * state.w + x]; +} + +/* draw(): basically the drawing function for everything. */ + +static void draw() +{ + int x, y; + + dclear(C_WHITE); + + /* draw the grid */ + + for (y = 0; y < 8; y++) { + for (x = 0; x < 16; x++) { + int tile = tile_cam(x, y); + int ox = x * 8, oy = y * 8; + + if (tile == 1 || tile == 2) { + /* above, below, left, right */ + if (tile_cam(x, y - 1) == 0) + dline(ox, oy, ox + 7, oy, C_BLACK); + if (tile_cam(x, y + 1) == 0) + dline(ox, oy + 7, ox + 7, oy + 7, C_BLACK); + if (tile_cam(x - 1, y) == 0) + dline(ox, oy, ox, oy + 7, C_BLACK); + if (tile_cam(x + 1, y) == 0) + dline(ox + 7, oy, ox + 7, oy + 7, C_BLACK); + + /* top left, top right, bottom left, bottom right */ + if (tile_cam(x - 1, y - 1) == 0) + dpixel(ox, oy, C_BLACK); + if (tile_cam(x + 1, y - 1) == 0) + dpixel(ox + 7, oy, C_BLACK); + if (tile_cam(x - 1, y + 1) == 0) + dpixel(ox, oy + 7, C_BLACK); + if (tile_cam(x + 1, y + 1) == 0) + dpixel(ox + 7, oy + 7, C_BLACK); + } + } + } + + /* Draw the enemies. */ + + { + struct enemy *ep; + extern bopti_image_t img_enemy; + + for (ep = state.first_enemy; ep; ep = ep->pdr) + dimage(ep->x, ep->y, &img_enemy); + } + + /* Draw the cursor */ + + { + int d = state.drf & 128 ? 2 : 0; + int ox = state.cur_x - state.cam_x + 1; + int oy = state.cur_y - state.cam_y + 1; + + dline(ox + d, oy, ox + d + 1, oy, C_BLACK); + dline(ox + d + 4, oy, ox + d + 5, oy, C_BLACK); + dline(ox, oy + d, ox, oy + d + 1, C_BLACK); + dline(ox, oy + d + 4, ox, oy + d + 5, C_BLACK); + + dline(ox + d, oy + 7, ox + d + 1, oy + 7, C_BLACK); + dline(ox + d + 4, oy + 7, ox + d + 5, oy + 7, C_BLACK); + dline(ox + 7, oy + d, ox + 7, oy + d + 1, C_BLACK); + dline(ox + 7, oy + d + 4, ox + 7, oy + d + 5, C_BLACK); + } + + /* okay we can update now */ + + dprint(16, 96, C_BLACK, "state.tnsp = 0x%08X", state.tnsp); + dupdate(); } /* Main game function. */ menu_t *game(menu_t *last_menu) { + int timeout = 1 /* a non-zero value just in case */; + extern uint8_t map_basic[]; + (void)last_menu; - dclear(C_WHITE); - dtext(1, 1, C_BLACK, "Sample fxSDK add-in."); - dupdate(); - getkey(); + init(map_basic); + while (1) { + int should_draw = 0; + key_event_t event = getkey_opt(GETKEY_MOD_SHIFT | GETKEY_BACKLIGHT, + &timeout); + + /* TODO: find a way to creatively deduce the time it takes to + * go from here to the `if (should_draw)`, in order to keep + * an as-constant-as-possible draw time */ + + switch (event.type) { + case KEYEV_NONE: + /* timeout event */ + should_draw = 1; + break; + case KEYEV_DOWN: + if (event.key == KEY_EXE) + return no_menu; + } + + if (timeout < 0 || should_draw) { + timeout = 65; + state.drf = (state.drf + 1) & 255; + + /* Here, we should draw. */ + tick(); /* TODO: perhaps a better placement somewhere */ + draw(); + } + } return no_menu; }