commit f7a027d6bf42416292e2e5cc526750b420558ee1 Author: Lephenixnoir Date: Sat Nov 19 17:38:40 2022 +0100 initial CPC version (with some Azur refactoring) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c4f84b --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Build files +/build-fx +/build-cg +/*.g1a +/*.g3a + +# Python bytecode + __pycache__/ + +# Common IDE files +*.sublime-project +*.sublime-workspace +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d36ba48 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.15) +project(Afterburner VERSION 1.0 LANGUAGES CXX C ASM) + +include(GenerateG3A) +include(Fxconv) + +find_package(Azur 0.1 REQUIRED) +find_package(Gint 2.8 REQUIRED) +find_package(LibProf 2.1 REQUIRED) + +set(SOURCES + src/camera.cpp + src/image.cpp + src/main.cpp + src/horizon.cpp + src/horizon.s + src/world.cpp +) +set(ASSETS + assets-cg/plane.png + assets-cg/square.png + assets-cg/dot.png + + assets-cg/tree_1.png + assets-cg/tree_2.png + assets-cg/tree_3.png + assets-cg/mountain_1.png + assets-cg/rock_1.png + assets-cg/rock_2.png + assets-cg/base_1.png + assets-cg/base_2.png + assets-cg/base_3.png + assets-cg/minimap.png + assets-cg/expl_1.png + assets-cg/expl_2.png + assets-cg/expl_3.png + assets-cg/expl_4.png + assets-cg/font.png +) + +fxconv_declare_assets(${ASSETS} WITH_METADATA) + +add_executable(afterburner ${SOURCES} ${ASSETS}) +target_compile_options(afterburner PRIVATE -Wall -Wextra -Os -std=c++20) +target_link_options(afterburner PRIVATE -Wl,-Map=map -Wl,--print-memory-usage) +target_link_libraries(afterburner Azur::Azur -lnum LibProf::LibProf Gint::Gint) + +generate_g3a(TARGET afterburner OUTPUT "AfterBur.g3a" + NAME "Afterburner" ICONS assets-cg/icon-uns.png assets-cg/icon-sel.png) diff --git a/TODO b/TODO new file mode 100644 index 0000000..0cf15c2 --- /dev/null +++ b/TODO @@ -0,0 +1,7 @@ +TODO: +* Title screen +* Multiple games +* Limit number of bombs +* Fix map boundary tests +* More interesting map generation +* Expand gameplay: missions, enemies, etc diff --git a/assets-cg/base_1.png b/assets-cg/base_1.png new file mode 100644 index 0000000..836ad9e Binary files /dev/null and b/assets-cg/base_1.png differ diff --git a/assets-cg/base_2.png b/assets-cg/base_2.png new file mode 100644 index 0000000..fdbc96e Binary files /dev/null and b/assets-cg/base_2.png differ diff --git a/assets-cg/base_3.png b/assets-cg/base_3.png new file mode 100644 index 0000000..6f7b5ee Binary files /dev/null and b/assets-cg/base_3.png differ diff --git a/assets-cg/dot.png b/assets-cg/dot.png new file mode 100644 index 0000000..f635d6f Binary files /dev/null and b/assets-cg/dot.png differ diff --git a/assets-cg/expl_1.png b/assets-cg/expl_1.png new file mode 100644 index 0000000..f4d838a Binary files /dev/null and b/assets-cg/expl_1.png differ diff --git a/assets-cg/expl_2.png b/assets-cg/expl_2.png new file mode 100644 index 0000000..c79ea6b Binary files /dev/null and b/assets-cg/expl_2.png differ diff --git a/assets-cg/expl_3.png b/assets-cg/expl_3.png new file mode 100644 index 0000000..30789c0 Binary files /dev/null and b/assets-cg/expl_3.png differ diff --git a/assets-cg/expl_4.png b/assets-cg/expl_4.png new file mode 100644 index 0000000..b96d0d5 Binary files /dev/null and b/assets-cg/expl_4.png differ diff --git a/assets-cg/font.png b/assets-cg/font.png new file mode 100644 index 0000000..3cf3776 Binary files /dev/null and b/assets-cg/font.png differ diff --git a/assets-cg/fxconv-metadata.txt b/assets-cg/fxconv-metadata.txt new file mode 100644 index 0000000..ca55401 --- /dev/null +++ b/assets-cg/fxconv-metadata.txt @@ -0,0 +1,7 @@ +*.png: + type: bopti-image + name_regex: (.*)\.png img_\1 + profile: p8_rgb565a + +minimap.png: + profile: rgb565a diff --git a/assets-cg/icon-sel.png b/assets-cg/icon-sel.png new file mode 100644 index 0000000..e5f1cf0 Binary files /dev/null and b/assets-cg/icon-sel.png differ diff --git a/assets-cg/icon-uns.png b/assets-cg/icon-uns.png new file mode 100644 index 0000000..0b0fdde Binary files /dev/null and b/assets-cg/icon-uns.png differ diff --git a/assets-cg/icon.xcf b/assets-cg/icon.xcf new file mode 100644 index 0000000..c2902b1 Binary files /dev/null and b/assets-cg/icon.xcf differ diff --git a/assets-cg/minimap.png b/assets-cg/minimap.png new file mode 100644 index 0000000..3d9be3a Binary files /dev/null and b/assets-cg/minimap.png differ diff --git a/assets-cg/mountain_1.png b/assets-cg/mountain_1.png new file mode 100644 index 0000000..3332da2 Binary files /dev/null and b/assets-cg/mountain_1.png differ diff --git a/assets-cg/plane.png b/assets-cg/plane.png new file mode 100644 index 0000000..f609800 Binary files /dev/null and b/assets-cg/plane.png differ diff --git a/assets-cg/rock_1.png b/assets-cg/rock_1.png new file mode 100644 index 0000000..bedb7ae Binary files /dev/null and b/assets-cg/rock_1.png differ diff --git a/assets-cg/rock_2.png b/assets-cg/rock_2.png new file mode 100644 index 0000000..d41de94 Binary files /dev/null and b/assets-cg/rock_2.png differ diff --git a/assets-cg/square.png b/assets-cg/square.png new file mode 100644 index 0000000..cbef44c Binary files /dev/null and b/assets-cg/square.png differ diff --git a/assets-cg/tree_1.png b/assets-cg/tree_1.png new file mode 100644 index 0000000..d015968 Binary files /dev/null and b/assets-cg/tree_1.png differ diff --git a/assets-cg/tree_2.png b/assets-cg/tree_2.png new file mode 100644 index 0000000..ad68571 Binary files /dev/null and b/assets-cg/tree_2.png differ diff --git a/assets-cg/tree_3.png b/assets-cg/tree_3.png new file mode 100644 index 0000000..2d1b948 Binary files /dev/null and b/assets-cg/tree_3.png differ diff --git a/src/afterburner.h b/src/afterburner.h new file mode 100644 index 0000000..de20885 --- /dev/null +++ b/src/afterburner.h @@ -0,0 +1,188 @@ +#ifndef AFTERBURNER_H +#define AFTERBURNER_H + +#include +#include +#include +#include +#include +#include +#include +using namespace libnum; + +//--- +// Horizon shader +//--- + +/* Render the horizon line with a palette. The horizon line passes through + (x,y) and has the specified angle. This is an Azur shader, the function only + queues a command and delays the rendering to azrp_update(). */ +void ab_horizon(int x, int y, float alpha, uint16_t *palette); +/* Configuration call, to be performed at every scale update */ +void ab_horizon_configure(void); + +//--- +// Image utilities +//--- + +/* Allocate a new image from a rotation + upscaling of a square image img. + The source image must be RGB565A, P8_RGB565A or P4_RGB565A. */ +bopti_image_t *image_rotate_scale_square + (bopti_image_t const *img, float alpha, num gamma); + +/* Same but optimized for P8_RGB565A. *anchor_x and *anchor_y should be set to + the coordinates of the "anchor" within the source image, and are updated to + reflect the coordinates of that same spot in the rotated image. */ +bopti_image_t *image_rotate_scale_square_p8_rgb565a + (bopti_image_t const *img, + float alpha, num gamma, + int *anchor_x, int *anchor_y); + +/* An image with all upscaled variations from 0.125x to 2x. */ +class DynamicImage +{ +public: + DynamicImage(bopti_image_t const *source, float m_alpha, prof_t *prof_ctx, + int anchor_x, int anchor_y); + ~DynamicImage(); + + /* Get the closest scaled version, generating it on demand. */ + bopti_image_t *getAtScale(num scale, int *anchor_x, int *anchor_y); + +private: + bopti_image_t *m_images[16]; + int m_anchor_x, m_anchor_y; + int8_t m_ax[16], m_ay[16]; + + bopti_image_t const *m_source; + float m_alpha; + prof_t *m_prof; +}; + +//--- +// 3D transforms and projections +//--- + +struct plane +{ + plane() { + memset(this, 0, sizeof *this); + } + + /* Position in space */ + vec3 pos; + /* Euler angles with aviation terminology */ + float roll, pitch, yaw; + /* Air speed */ + num air_speed; + + // The following are automatically set by plane_update() + + /* Sine and cosine of every angle */ + num cos_r, sin_r, cos_p, sin_p, cos_y, sin_y; + /* Vector looking forward and to the right */ + vec3 forward, right; + /* Vector looking forward but parallel to the ground */ + vec3 forward_ground; +}; + +struct mat3 +{ + num x11, x12, x13; + num x21, x22, x23; + num x31, x32, x33; +}; + +mat3 operator * (mat3 const &A, mat3 const &B); +vec3 operator * (mat3 const &M, vec3 const &u); +num distance2(vec3 u, vec3 v); +num64 distance2_64(vec3 u, vec3 v); + +/* Update computationally-derived parameters of a plane */ +void plane_update(struct plane *plane); + +/* Pixels per world unit (primitive control for FOV) */ +#define SCREEN_SCALING 200 +/* Minimum and maximum distance of visible objects */ +#define NEAR_CLIP_PLANE 8 +#define FAR_CLIP_PLANE 50 +#define SIDE_CLIP_PLANE 25 +/* Margin around the screen where a center point is considered visible */ +#define SCREEN_MARGIN 20 + +/* A 3D point with a handle to some sprite/information/etc. */ +struct ref_point +{ + /* Position of the point in 3D space (evolves during transformations) */ + vec3 pos; + /* Unique identifier (used to identify point properties in world map) */ + int id; +}; + +void transform_world2camera(struct plane const *plane, vec3 camera_pos, + struct ref_point *points, int size); + +void transform_camera2screen(struct ref_point *points, int size); + +//--- +// World map +//--- + +/* Needs to be larger than FAR_CLIP_PLANE for culling optimisations to work */ +#define CHUNK_SIZE 64 + +struct chunk +{ + struct ref_point *points; + int count; +}; + +struct world +{ + int w, h; + int chunks_x, chunks_y; + struct chunk *chunks; +}; + +/* Make a world structure using the specified set of points. The points are + split into chunks, which allows faster lookup later on. The points are all + copied, to the original array is safe to free after this call. */ +struct world *world_make(int w, int h, struct ref_point *points, int count); + +/* Destroy a world structure and its copied points. */ +void world_destroy(struct world *w); + +/* Select the points in the world that are "close" to the specified anchor. + These are points from the anchor's chunk and up to 8 neighboring chunks. The + size of the array is returned in *size. free() the array after use. + + This function rotates chunks, so that for instance if position.x is 0.5 and + forward.x < 0 (ie. we look at a region of space with x<0), chunks from the + other side of the map are rotated in to provide a wrap-around effect. + + extra_size entries are left uninitialized at the end of the array so that + any dynamic sprites can be added. *size does *not* count extra_size. */ +struct ref_point *world_select(struct world const *w, vec3 position, + vec3 forward, int *size, int extra_size); + +//--- +// Game mechanics +//--- + +/* Distance forward where bombs drop */ +#define BOMB_DISTANCE 15 + +struct objective +{ + vec3 pos; + bool destroyed; + int type; +}; + +struct explosion +{ + vec3 pos; + num time; /* if negative, no explosion */ +}; + +#endif /* AFTERBURNER_H */ diff --git a/src/camera.cpp b/src/camera.cpp new file mode 100644 index 0000000..c268026 --- /dev/null +++ b/src/camera.cpp @@ -0,0 +1,112 @@ +#define __BSD_VISIBLE 1 +#include "afterburner.h" +#include +#include + +mat3 operator *(mat3 const &A, mat3 const &B) +{ + mat3 C; + + C.x11 = A.x11 * B.x11 + A.x12 * B.x21 + A.x13 * B.x31; + C.x12 = A.x11 * B.x12 + A.x12 * B.x22 + A.x13 * B.x32; + C.x13 = A.x11 * B.x13 + A.x12 * B.x23 + A.x13 * B.x33; + + C.x21 = A.x21 * B.x11 + A.x22 * B.x21 + A.x23 * B.x31; + C.x22 = A.x21 * B.x12 + A.x22 * B.x22 + A.x23 * B.x32; + C.x23 = A.x21 * B.x13 + A.x22 * B.x23 + A.x23 * B.x33; + + C.x31 = A.x31 * B.x11 + A.x32 * B.x21 + A.x33 * B.x31; + C.x32 = A.x31 * B.x12 + A.x32 * B.x22 + A.x33 * B.x32; + C.x33 = A.x31 * B.x13 + A.x32 * B.x23 + A.x33 * B.x33; + + return C; +} + +vec3 operator * (mat3 const &M, vec3 const &u) +{ + vec3 v; + + v.x = M.x11 * u.x + M.x12 * u.y + M.x13 * u.z; + v.y = M.x21 * u.x + M.x22 * u.y + M.x23 * u.z; + v.z = M.x31 * u.x + M.x32 * u.y + M.x33 * u.z; + + return v; +} + +num distance2(vec3 u, vec3 v) +{ + u -= v; + return u.x * u.x + u.y * u.y + u.z * u.z; +} + +num64 distance2_64(vec3 u, vec3 v) +{ + u -= v; + return num32::dmul(u.x,u.x) + num32::dmul(u.y,u.y) + num32::dmul(u.z,u.z); +} + +void plane_update(struct plane *plane) +{ + float c, s; + + sincosf(plane->roll, &s, &c); + plane->sin_r = s; + plane->cos_r = c; + + sincosf(plane->pitch, &s, &c); + plane->sin_p = s; + plane->cos_p = c; + + sincosf(plane->yaw, &s, &c); + plane->sin_y = s; + plane->cos_y = c; + + plane->forward = vec3( + -plane->sin_y * plane->cos_p, + plane->cos_y * plane->cos_p, + plane->sin_p); + plane->forward_ground = vec3(-plane->sin_y, plane->cos_y, 0); + plane->right = vec3(plane->cos_y, plane->sin_y, 0); +} + +void transform_world2camera(struct plane const *plane, vec3 camera_pos, + struct ref_point *points, int size) +{ + num cos_r = plane->cos_r; + num sin_r = plane->sin_r; + num cos_p = plane->cos_p; + num sin_p = plane->sin_p; + num cos_y = plane->cos_y; + num sin_y = plane->sin_y; + + mat3 m_roll = { + cos_r, 0, sin_r, + 0, 1, 0, + -sin_r, 0, cos_r, + }; + mat3 m_pitch = { + 1, 0, 0, + 0, cos_p, +sin_p, + 0, -sin_p, cos_p, + }; + mat3 m_yaw = { + cos_y, +sin_y, 0, + -sin_y, cos_y, 0, + 0, 0, 1, + }; + + mat3 M = m_roll * (m_pitch * m_yaw); + + for(int i = 0; i < size; i++) + points[i].pos = M * (points[i].pos - camera_pos); +} + +void transform_camera2screen(struct ref_point *points, int size) +{ + for(int i = 0; i < size; i++) { + vec3 r = points[i].pos; + r.x = num(azrp_width / 2) + r.x * SCREEN_SCALING / r.y; + r.z = num(azrp_height / 2) - r.z * SCREEN_SCALING / r.y; + points[i].pos = r; + } +} diff --git a/src/horizon.cpp b/src/horizon.cpp new file mode 100644 index 0000000..01425db --- /dev/null +++ b/src/horizon.cpp @@ -0,0 +1,60 @@ +#include "afterburner.h" +#include +#include +#include +using namespace libnum; + +uint8_t AB_HORIZON_SHADER_ID = -1; + +struct ab_horizon_cmd +{ + /* Shader ID for Azur */ + uint8_t shader_id; + uint8_t _[3]; + + /* Variation in value for a single move of +1x */ + num dx; + /* Variation in value for a row move of -198x and +1y */ + num drow; + /* Palette, supporting indexes 0...255 */ + uint16_t *palette; + /* Current horizon value */ + num current; +}; + +extern "C" { + extern azrp_shader_t ab_horizon_shader; +} + +GCONSTRUCTOR +static void register_shader(void) +{ + AB_HORIZON_SHADER_ID = azrp_register_shader(ab_horizon_shader); +} + +void ab_horizon(int cx, int cy, float alpha, uint16_t *palette) +{ + prof_enter(azrp_perf_cmdgen); + + num sin_alpha = sinf(alpha); + num cos_alpha = cosf(alpha); + + num dx = -sin_alpha; + num dy = -cos_alpha; + + struct ab_horizon_cmd cmd; + cmd.shader_id = AB_HORIZON_SHADER_ID; + cmd.current = dx * -cx + dy *-cy; + cmd.dx = dx; + cmd.drow = -198*dx + dy; + cmd.palette = palette; + + azrp_queue_command(&cmd, sizeof cmd, 0, azrp_frag_count); + prof_leave(azrp_perf_cmdgen); +} + +void ab_horizon_configure(void) +{ + uint32_t value = (azrp_width << 16) | azrp_frag_height; + azrp_set_uniforms(AB_HORIZON_SHADER_ID, (void *)value); +} diff --git a/src/horizon.s b/src/horizon.s new file mode 100644 index 0000000..93bf6c8 --- /dev/null +++ b/src/horizon.s @@ -0,0 +1,94 @@ +/* Horizon shader + + r0: (temporary) + r1: cmd.dx + r2: cmd.drow + r3: palette + r4: azrp_width + r5: cmd + r6: azrp_frag + r7: azrp_frag_height + r8: cmd.current + r9: a constant + r10: mask for color index access + */ + +.global _ab_horizon_shader +.balign 4 + +_ab_horizon_shader: + /* r4: azrp_width (top 16 bits) | azrp_frag_height (bottom 16 bits) + r5: struct ab_horizon_cmd *cmd + r6: uint16_t *fragment */ + + mov.l r8, @-r15 + add #4, r5 + + mov.l r9, @-r15 + extu.w r4, r7 + + mov.l @r5+, r1 /* cmd.dx */ + shlr16 r4 + + mov.l @r5+, r2 /* cmd.drow */ + add #-2, r6 + + mov.l @r5+, r3 /* cmd.palette */ + nop + + mov.l @r5, r8 /* num.current */ + nop + + mov.l r10, @-r15 /* r10 = 0x1ff (511 colors) */ + mov #-1, r10 + + ldrs 1f + mov #-23, r0 + + ldre 2f + shld r0, r10 + + mov.l .round, r9 + nop + +.row: + ldrc r4 + nop + +1: add #2, r6 + mov r8, r0 + + add r9, r0 + nop + + shlr16 r0 + nop + + and r10, r0 + nop + + shll r0 + mov.w @(r0, r3), r0 + + mov.w r0, @r6 +2: add r1, r8 + + dt r7 + nop + + bf.s .row + add r2, r8 + + /* Update cmd.current for the next fragment */ + mov.l r8, @r5 + nop + + mov.l @r15+, r10 + mov.l @r15+, r9 + rts + mov.l @r15+, r8 + +.balign 4 +.round: + /* Add 256 and also round up the next unit */ + .long 0x01008000 diff --git a/src/image.cpp b/src/image.cpp new file mode 100644 index 0000000..b79e2bd --- /dev/null +++ b/src/image.cpp @@ -0,0 +1,56 @@ +#include "afterburner.h" +#include +#include +#include +#include +#include + +//--- +// Dynamic images +//--- + +DynamicImage::DynamicImage(bopti_image_t const *source, float alpha, + prof_t *prof_ctx, int anchor_x, int anchor_y): + m_anchor_x {anchor_x}, m_anchor_y {anchor_y}, m_source {source}, + m_alpha {alpha}, m_prof {prof_ctx} +{ + for(int i = 0; i < 16; i++) { + m_images[i] = NULL; + m_ax[i] = -1; + m_ay[i] = -1; + } +} + +bopti_image_t *DynamicImage::getAtScale(num scale, int *ax, int *ay) +{ + if(m_prof) + prof_enter(*m_prof); + + /* Round to the closest multiple of 0.125 */ + int i = (int)(8 * scale + num(0.5)); + if(i < 0) i = 0; + if(i > 15) i = 15; + + if(!m_images[i]) { + int ax_i=m_anchor_x, ay_i=m_anchor_y; + struct image_linear_map map; + image_rotate_around_scale(m_source, m_alpha, scale.v, false, &ax_i, + &ay_i, &map); + m_images[i] = image_linear_alloc(m_source, &map); + m_ax[i] = ax_i; + m_ay[i] = ay_i; + } + + if(m_prof) + prof_leave(*m_prof); + + *ax = m_ax[i]; + *ay = m_ay[i]; + return m_images[i]; +} + +DynamicImage::~DynamicImage() +{ + for(int i = 0; i < 16; i++) + image_free(m_images[i]); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a27e789 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,722 @@ +#include "afterburner.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static char logbuf[512]; + +GUNUSED static void logger(char const *fmt, ...) +{ + if(!usb_is_open()) + return; + + va_list args; + va_start(args, fmt); + + vsnprintf(logbuf, sizeof logbuf, fmt, args); + va_end(args); + + usb_fxlink_text(logbuf, 0); +} + +#define RGB24(hex) \ + (((hex & 0xf80000) >> 8) | \ + ((hex & 0x00fc00) >> 5) | \ + ((hex & 0x0000f8) >> 3)) + +/* Generate the gradient used for the horizon. This is an array of 512 colors, + 256 of which represent the ground, while the other 256 represent the sky. */ +static void generate_horizon_gradient(uint16_t *horizon_colors) +{ + int hc = 0; + + for(int i = 0; i < 256; i++) { + horizon_colors[hc++] = RGB24(0x438747); + } + for(int i = 0; i < 32; i++) { + int r = 0x68 + (0x3f - 0x68) * i / 32; + int g = 0x8c + (0x66 - 0x8c) * i / 32; + int b = 0xd6 + (0xb3 - 0xd6) * i / 32; + horizon_colors[hc++] = RGB24(((r << 16) | (g << 8) | b)); + } + for(int i = 0; i < 64; i++) { + horizon_colors[hc++] = RGB24(0x3f66b3); + }; + for(int i = 0; i < 64; i++) { + int r = 0x3f + (0x0c - 0x3f) * i / 64; + int g = 0x66 + (0x29 - 0x66) * i / 64; + int b = 0xb3 + (0x61 - 0xb3) * i / 64; + horizon_colors[hc++] = RGB24(((r << 16) | (g << 8) | b)); + } + for(int i = 0; i < 96; i++) { + horizon_colors[hc++] = RGB24(0x0c2961); + } +} + +/* A plane controller that individually controls each direction. + * Up/Down: move forward/backward (including pitch) + * Left/Right: move left/right (ignoring roll; parallel to the ground) + * SHIFT+Up/Down: pitch down/up + * SHIFT+Left/Right: turn left/right (yaw) + * ALPHA+Up/Down: move forward/backward (ignoring pitch) + * ALPHA+Left/Right: roll */ +GUNUSED static void controller_debug(struct plane *plane, num dt) +{ + int sh = keydown(KEY_SHIFT); + int al = keydown(KEY_ALPHA); + + float dt_f = (float)dt; + + /* Movement speed for movement controlled here */ + num debug_speed = 6.0; + + if(keydown(KEY_UP) && !sh && !al) + plane->pos += plane->forward * debug_speed * dt; + if(keydown(KEY_DOWN) && !sh && !al) + plane->pos -= plane->forward * debug_speed * dt; + if(keydown(KEY_RIGHT) && !sh && !al) + plane->pos += plane->right * debug_speed * dt; + if(keydown(KEY_LEFT) && !sh && !al) + plane->pos -= plane->right * debug_speed * dt; + + if(keydown(KEY_UP) && sh && !al) + plane->pitch -= 0.3 * dt_f; + if(keydown(KEY_DOWN) && sh && !al) + plane->pitch += 0.3 * dt_f; + if(keydown(KEY_RIGHT) && sh && !al) + plane->yaw -= 0.5 * dt_f; + if(keydown(KEY_LEFT) && sh && !al) + plane->yaw += 0.5 * dt_f; + + if(keydown(KEY_UP) && !sh && al) + plane->pos += plane->forward_ground * debug_speed * dt; + if(keydown(KEY_DOWN) && !sh && al) + plane->pos -= plane->forward_ground * debug_speed * dt; + if(keydown(KEY_LEFT) && !sh && al) + plane->roll += 0.45 * dt_f; + if(keydown(KEY_RIGHT) && !sh && al) + plane->roll -= 0.45 * dt_f; + + if(keydown(KEY_MINUS)) + plane->pos.z -= 2.0 * dt; + if(keydown(KEY_PLUS)) + plane->pos.z += 2.0 * dt; +} + +/* A simple plane controller for players. + * Up/Down: pitch down/up + * Left/Right: turn left/right (yaw) + * SHIFT/ALPHA: accelerate/decelerate + TODO: Automatic roll effect when turning */ +static void controller_simple(struct plane *plane, num dt) +{ + float dt_f = (float)dt; + + if(keydown(KEY_UP)) + plane->pitch -= 0.3 * dt_f; + if(keydown(KEY_DOWN)) + plane->pitch += 0.3 * dt_f; + + if(keydown(KEY_RIGHT)) + plane->yaw -= 0.5 * dt_f; + if(keydown(KEY_LEFT)) + plane->yaw += 0.5 * dt_f; + + if(keydown(KEY_SHIFT)) + plane->air_speed += num(4.0) * dt; + if(keydown(KEY_ALPHA)) + plane->air_speed -= num(4.0) * dt; + + /* Lock roll to player turning, for more immersion and visual effects + while keeping very simple controls */ + float target_roll = 0; + if(keydown(KEY_LEFT)) + target_roll = 0.5; + if(keydown(KEY_RIGHT)) + target_roll = -0.5; + + if(plane->roll < target_roll) { + plane->roll += 0.3 * dt_f; + if(plane->roll > target_roll) + plane->roll = target_roll; + } + if(plane->roll > target_roll) { + plane->roll -= 0.3 * dt_f; + if(plane->roll < target_roll) + plane->roll = target_roll; + } +} + +/* Sort to have closer objects last */ +static int compare_depth(void const *p1, void const *p2) +{ + struct ref_point const *u = (struct ref_point const *)p1; + struct ref_point const *v = (struct ref_point const *)p2; + /* Return v < u */ + return v->pos.y.v - u->pos.y.v; +} + +#define remap(point, _x, _y) { \ + *(_x) = wx + (int)((point).x) * ww / world->w; \ + *(_y) = wy + wh-1 - (int)((point).y) * wh / world->h; \ +} + +/* Generate the minimap as a bopti image (Azur can't easily draw like in the + VRAM, and I don't have time to write a full shader for that) */ +static bopti_image_t *generate_minimap(struct world const *world, + struct plane const *plane, struct objective *objectives, + int objective_count) +{ + extern bopti_image_t img_minimap; + + /* Duplicate the empty minimap (which is full RGB565) */ + image_t *img = image_copy_alloc(&img_minimap, IMAGE_RGB565A); + + /* Location of world map within image */ + int wx=9, wy=9, ww=30, wh=30, x, y; + + for(int i = 0; i < objective_count; i++) { + remap(objectives[i].pos, &x, &y); + int color = objectives[i].destroyed ? C_RGB(15,3,31) : C_RGB(31,2,5); + image_set_pixel(img, x, y, color); + } + + for(int i = 0; i < 32; i++) { + remap(plane->pos + num(i) * plane->forward, &x, &y); + image_set_pixel(img, x, y, C_RGB(0, 18, 4)); + } + + remap(plane->pos, &x, &y); + image_set_pixel(img, x, y, C_RGB(0, 31, 10)); + + return img; +} + +/* Draw player's plane at (x,y) */ +static void draw_player(int x, int y, struct plane *plane, + bool orient_with_angles, bool orient_with_keyboard) +{ + /* Rows: 25 (anchor 17), 27 (anchor 20), 26 (anchor 21) + Columns: all 48 (anchor 23) */ + extern bopti_image_t img_plane; + + int row = 1, col = 1; + if(orient_with_angles) { + if(plane->pitch > 0.08) + row = 0; + if(plane->pitch < -0.08) + row = 2; + if(plane->roll > 0.1) + col = 0; + if(plane->roll < -0.1) + col = 2; + } + if(orient_with_keyboard) { + if(keydown(KEY_DOWN)) + row = 0; + if(keydown(KEY_UP)) + row = 2; + if(keydown(KEY_LEFT)) + col = 0; + if(keydown(KEY_RIGHT)) + col = 2; + } + + int top=0, height=0, ay=0; + if(row == 0) top = 0, height = 25, ay = 17; + if(row == 1) top = 25, height = 27, ay = 20; + if(row == 2) top = 52, height = 26, ay = 21; + + azrp_subimage(x-23, y-ay, &img_plane, 48 * col, top, 48, height, + DIMAGE_NONE); +} + +/* Distance between two points, with wrap-around */ +num64 distance2_wrap_64(struct world const *world, vec3 u, vec3 v) +{ + num dx = u.x - v.x; + num dy = u.y - v.y; + num dz = u.z - v.z; + + /* Choose the closest x distance possible */ + if(dx < num(-world->w)) + dx += num(world->w); + if(dx > num(world->w)) + dx -= num(world->w); + /* Choose the closest y distance possible */ + if(dy < num(-world->h)) + dy += num(world->h); + if(dy > num(world->h)) + dy -= num(world->h); + + return num32::dmul(dx, dx) + num32::dmul(dy, dy) + num32::dmul(dz, dz); +} + +/* Render text with Azur images - quite bad, but I don't have time lol. */ +static void draw_text(int x, int y, char const *fmt, ...) +{ + char str[128]; + va_list args; + va_start(args, fmt); + vsnprintf(str, 128, fmt, args); + va_end(args); + + extern bopti_image_t img_font; + + for(int i = 0; str[i]; i++) { + if(str[i] < 32 || str[i] >= 0x7f) continue; + + int row = (str[i] - 32) >> 4; + int col = (str[i] - 32) & 15; + azrp_subimage(x + 5 * i, y, &img_font, 7 * col + 1, 9 * row + 1, 6, 8, + DIMAGE_NONE); + } +} + +int main(void) +{ + azrp_config_scale(2); + azrp_shader_clear_configure(); + azrp_shader_image_rgb16_configure(); + azrp_shader_image_p8_configure(); + azrp_shader_image_p4_configure(); + ab_horizon_configure(); + + __printf_enable_fp(); + + srand(rtc_ticks()); + + prof_init(); + + usb_interface_t const *intf[] = { &usb_ff_bulk, NULL }; + usb_open(intf, GINT_CALL_NULL); + + int volatile timer_flag = 1; + int timer = timer_configure(TIMER_ANY, 33000, GINT_CALL_SET(&timer_flag)); + timer_start(timer); + + bool lost = false; + + //--- + // Horizon colors + //--- + + uint16_t horizon_colors[512]; + generate_horizon_gradient(horizon_colors); + + //--- + // World map + //--- + + /* Stuff starts to blow up at the density of 1/16 sprite/unit² (4096 + sprites for a 256x256 world): even with chunk-based selection, we end up + with > 2000 points to consider each frame, which makes the 3D transform + blow up to 16-20 ms per frame. That's the obvious next optimization. */ + int point_count = 2048; + struct ref_point *points = (struct ref_point *) + malloc(point_count * sizeof *points); + + /* We give 12 bases to destroy */ + struct objective objectives[12]; + int objective_count = 12; + + /* We allow 3 explosions at the same time */ + struct explosion explosions[3]; + int explosion_count = 3; + + for(int i = 0; i < explosion_count; i++) { + explosions[i].pos = vec3(); + explosions[i].time = num(-1); + } + + for(int i = 0; i < point_count; i++) { + num x, y; + x.v = rand() % num(256).v; + y.v = rand() % num(256).v; + points[i].pos = vec3(x, y, 0); + + if(i < objective_count) { + objectives[i].pos = points[i].pos; + objectives[i].destroyed = false; + objectives[i].type = rand() % 3; + points[i].id = 100 + i; + } + else { + points[i].id = rand() % 6; + } + } + + struct world *world = world_make(256, 256, points, point_count); + if(!world) return 0; + + //--- + // Cached explosion images + //--- + + prof_t perf_scaling; + + /* Unlike normal sprites, we don't rotate exlosions, so we can keep them + cached for the whole execution */ + extern bopti_image_t img_expl_1, img_expl_2, img_expl_3, img_expl_4; + DynamicImage dyn_expl_1(&img_expl_1, 0.0, &perf_scaling, 24, 24); + DynamicImage dyn_expl_2(&img_expl_2, 0.0, &perf_scaling, 24, 24); + DynamicImage dyn_expl_3(&img_expl_3, 0.0, &perf_scaling, 24, 24); + DynamicImage dyn_expl_4(&img_expl_4, 0.0, &perf_scaling, 31, 31); + + DynamicImage *dyn_expl[4] = { + &dyn_expl_1, &dyn_expl_2, &dyn_expl_3, &dyn_expl_4, + }; + + //--- + // HUD and messages + //--- + + char const *message = NULL; + num message_time = num(0.0); + + //--- + // Camera + //--- + + struct plane plane; + plane.pos = vec3(world->w / 2, world->h / 2, 5); + plane.roll = 0.0f; + plane.pitch = 0.0f; + plane.yaw = 0.0f; + plane.air_speed = 12.0; + + int plane_screen_x = 198/2; + int plane_screen_y = 112/2; + + while(1) { + /* Wait until a new frame to cap at the desired FPS */ + while(!timer_flag) sleep(); + + //--- Total frame time + prof_t perf_frame = prof_make(); + prof_enter(perf_frame); + + /* Time difference for this frame (assumes consistant FPS) */ + num dt = 33e-3; + + //--- Physics/modeling time (split in two parts) + prof_t perf_physics = prof_make(); + prof_enter(perf_physics); + + plane_update(&plane); + vec3 camera_pos = plane.pos - num(10) * plane.forward; + + /* Compute horizon reference */ + struct ref_point far_forward; + far_forward.pos = plane.pos + plane.forward * num(FAR_CLIP_PLANE); + far_forward.pos.z = 0; + transform_world2camera(&plane, camera_pos, &far_forward, 1); + transform_camera2screen(&far_forward, 1); + + int visible_explosions = 0; + for(int i = 0; i < explosion_count; i++) + visible_explosions += (explosions[i].time >= num(0)); + + /* Gather world points in chunks close to the plane */ + int sprite_count; + struct ref_point *sprites = world_select(world, plane.pos, + plane.forward, &sprite_count, 1 + visible_explosions); + + /* Extra sprite for the target under the plane */ + sprites[sprite_count].pos = + plane.pos + num(BOMB_DISTANCE) * plane.forward; + sprites[sprite_count].pos.z = 0; + sprites[sprite_count].id = -1; + sprite_count++; + + /* Extra sprites for visible explosions */ + for(int i = 0; i < explosion_count; i++) { + if(explosions[i].time < num(0)) + continue; + + int n = (int)(explosions[i].time * 6) % 4; + sprites[sprite_count].pos = explosions[i].pos; + sprites[sprite_count].id = 9 + n; + sprite_count++; + } + + transform_world2camera(&plane, camera_pos, sprites, sprite_count); + + /* Prune sprites that are definitely not visible */ + int close_point_count = 0; + for(int i = 0; i < sprite_count; i++) { + if(sprites[i].pos.y < num(NEAR_CLIP_PLANE) || + sprites[i].pos.y >= num(FAR_CLIP_PLANE) || + abs((int)sprites[i].pos.x) >= SIDE_CLIP_PLANE) + continue; + sprites[close_point_count++] = sprites[i]; + } + + transform_camera2screen(sprites, close_point_count); + + /* Prune points that are also too far awar from the screen */ + int visible_point_count = 0; + for(int i = 0; i < close_point_count; i++) { + int x = (int)sprites[i].pos.x; + int z = (int)sprites[i].pos.z; + if(x < -SCREEN_MARGIN || x >= azrp_width + SCREEN_MARGIN + || z < -SCREEN_MARGIN || z >= azrp_height + SCREEN_MARGIN) + continue; + sprites[visible_point_count++] = sprites[i]; + } + + /* Sort sprites furthest to closest */ + qsort(sprites, visible_point_count, sizeof *sprites, compare_depth); + + prof_leave(perf_physics); + + extern bopti_image_t img_dot; + extern bopti_image_t img_tree_1, img_tree_2, img_tree_3; + extern bopti_image_t img_mountain_1; + extern bopti_image_t img_rock_1, img_rock_2; + extern bopti_image_t img_base_1, img_base_2, img_base_3; + + /* Resize images for all visible sprites */ + perf_scaling = prof_make(); + + DynamicImage dyn_tree_1(&img_tree_1, + -plane.roll, &perf_scaling, 16, 31); + DynamicImage dyn_tree_2(&img_tree_2, + -plane.roll, &perf_scaling, 16, 31); + DynamicImage dyn_tree_3(&img_tree_3, + -plane.roll, &perf_scaling, 16, 31); + DynamicImage dyn_mountain_1(&img_mountain_1, + -plane.roll, &perf_scaling, 24, 34); + DynamicImage dyn_rock_1(&img_rock_1, + -plane.roll, &perf_scaling, 15, 20); + DynamicImage dyn_rock_2(&img_rock_2, + -plane.roll, &perf_scaling, 11, 15); + DynamicImage dyn_base_1(&img_base_1, + -plane.roll, &perf_scaling, 16, 20); + DynamicImage dyn_base_2(&img_base_2, + -plane.roll, &perf_scaling, 16, 20); + DynamicImage dyn_base_3(&img_base_3, + -plane.roll, &perf_scaling, 16, 20); + + DynamicImage *dyn_images[9] = { + &dyn_tree_1, &dyn_tree_2, &dyn_tree_3, + &dyn_mountain_1, + &dyn_rock_1, &dyn_rock_2, + &dyn_base_1, &dyn_base_2, &dyn_base_3, + }; + + /* Generate the minimap */ + image_t *minimap = generate_minimap(world, &plane, objectives, + objective_count); + + //--- + + azrp_perf_clear(); + ab_horizon((int)far_forward.pos.x, (int)far_forward.pos.z, -plane.roll, + horizon_colors); + + for(int i = 0; i < visible_point_count; i++) { + int ax, ay; + + num scale = 15.0; + /* Make targeted buildings larger */ + if(sprites[i].id >= 6 && sprites[i].id < 9) + scale = 25.0; + scale = scale / sprites[i].pos.y; + + bopti_image_t *img = NULL; + if(sprites[i].id == -1) { + img = &img_dot; + ax = 2; + ay = 2; + } + else if(sprites[i].id < 6) { + img = dyn_images[sprites[i].id]->getAtScale(scale,&ax,&ay); + } + else if(sprites[i].id >= 9 && sprites[i].id < 13) { + img = dyn_expl[sprites[i].id - 9]->getAtScale(scale, &ax, &ay); + } + else if(sprites[i].id >= 100) { + struct objective *o = &objectives[sprites[i].id - 100]; + if(!o->destroyed) + img = dyn_images[6+o->type]->getAtScale(scale,&ax,&ay); + } + + if(img) azrp_image((int)sprites[i].pos.x - ax, + (int)sprites[i].pos.z - ay, img); + } + if(!lost) + draw_player(plane_screen_x, plane_screen_y, &plane, false, !lost); + if(message) + draw_text(2, 2, "%s", message); + else + draw_text(2, 2, "Speed:%d", (int)(plane.air_speed * 40)); + azrp_image(azrp_width - minimap->width - 2, 2, minimap); + + /* Shows a dot the forward direction on the ground */ + // azrp_image((int)far_forward.x-2, (int)far_forward.z-2, &img_dot); + azrp_update(); + + free(sprites); + image_free(minimap); + + //--- + // Event handling and physics + //--- + + bool explosion_trigger = false; + key_event_t ev; + while((ev = pollevent()).type != KEYEV_NONE) { + if(ev.type == KEYEV_DOWN && ev.key == KEY_F1) + explosion_trigger = true; + } + + clearevents(); + if(keydown(KEY_MENU) || keydown(KEY_EXIT)) + break; + + prof_enter(perf_physics); + + if(!lost) + controller_simple(&plane, dt); + + /* Limit pitch to reasonable ranges (extreme values cause graphical + glitches with the horizon, and accentuate the lens problem */ + if(plane.pitch < -0.2) + plane.pitch = -0.2; + if(plane.pitch > 0.2) + plane.pitch = 0.2; + + /* Limit air speed to "reasonable" defaults */ + if(plane.air_speed < num(6.0)) + plane.air_speed = 6.0; + if(plane.air_speed > num(24.0)) + plane.air_speed = 24.0; + + if(!lost) + plane.pos += plane.air_speed * dt * plane.forward; + + /* Wrap the plane around the world map */ + if((int)plane.pos.x < 0) + plane.pos.x += world->w; + if((int)plane.pos.x >= world->w) + plane.pos.x -= world->w; + if((int)plane.pos.y < 0) + plane.pos.y += world->h; + if((int)plane.pos.y >= world->h) + plane.pos.y -= world->h; + + /* Add new explosions */ + if(!lost && explosion_trigger) { + int id = -1; + for(int i = 0; i < explosion_count; i++) { + if(explosions[i].time < num(0)) { + id = i; + break; + } + } + if(id >= 0) { + explosions[id].pos = + plane.pos + num(BOMB_DISTANCE) * plane.forward; + explosions[id].pos.z = 0; + explosions[id].time = 0.0; + + bool hit_someone = false; + + /* Destroy bases that are close! */ + for(int i = 0; i < objective_count; i++) { + if(distance2_64(objectives[i].pos, + explosions[id].pos) < num64(20)) { + objectives[i].destroyed = true; + hit_someone = true; + } + } + + message = hit_someone ? "Hit!" : "Missed!"; + message_time = num(2.0); + } + } + + /* Maintain the time of current explosions */ + for(int i = 0; i < explosion_count; i++) { + if(explosions[i].time >= num(0)) { + explosions[i].time += dt; + if(explosions[i].time >= num(4.0/6.0)) + explosions[i].time = -1; + } + } + + /* Maintain the message time */ + if(message != NULL) { + message_time -= dt; + if(message_time < num(0)) { + message = NULL; + message_time = num(0.0); + } + } + + prof_leave(perf_physics); + prof_leave(perf_frame); +#if 0 + logger("Frame total: %d µs\n - Rendering: update %d µs, shaders %d µs" + "\n - Physics: %d µs\n - Sprite scaling: %d µs\n" + "Current info:\n Position: x=%g y=%g z=%g\n Angles: roll=%g " + "pitch=%g yaw=%g\n Air speed: %g\n" + "Points: %d selected, %d close, %d visible\n" + "Explosions: %d\n", + prof_time(perf_frame), + prof_time(azrp_perf_render), + prof_time(azrp_perf_shaders), + prof_time(perf_physics), + prof_time(perf_scaling), + (double)plane.pos.x, + (double)plane.pos.y, + (double)plane.pos.z, + (double)plane.roll, + (double)plane.pitch, + (double)plane.yaw, + (double)plane.air_speed, + sprite_count, + close_point_count, + visible_point_count, + visible_explosions); +#endif + + /* Victory condition */ + bool all_objectives_destroyed = true; + for(int i = 0; i < objective_count; i++) + all_objectives_destroyed &= objectives[i].destroyed; + + if(all_objectives_destroyed) { + message = "Mission accomplished!"; + message_time = num(32000); + } + + /* Death condition */ + if(!lost && plane.pos.z <= num(0.5)) { + message = "Unfortunate."; + message_time = num(32000); + lost = true; + + /* Hijack one of the explosions */ + explosions[0].pos = plane.pos; + explosions[0].time = num(0.0); + } + } + + timer_stop(timer); + return 1; +} diff --git a/src/world.cpp b/src/world.cpp new file mode 100644 index 0000000..2eecc5f --- /dev/null +++ b/src/world.cpp @@ -0,0 +1,163 @@ +#include "afterburner.h" +#include + +int debug = 0; +void *debug2 = NULL; + +struct world *world_make(int width, int height, struct ref_point *points, + int count) +{ + if(!points) + return NULL; + + struct world *w = (struct world *)malloc(sizeof *w); + int *js = NULL; + size_t s; + if(!w) goto fail; + + w->w = width; + w->h = height; + w->chunks_x = (width + CHUNK_SIZE - 1) / CHUNK_SIZE; + w->chunks_y = (height + CHUNK_SIZE - 1) / CHUNK_SIZE; + + s = w->chunks_x * w->chunks_y * sizeof *w->chunks; + w->chunks = (struct chunk *)malloc(s); + if(!w->chunks) goto fail; + for(int i = 0; i < w->chunks_x * w->chunks_y; i++) { + w->chunks[i].points = NULL; + w->chunks[i].count = 0; + } + + /* Do a first pass to count the number of points in each chunks */ + for(int i = 0; i < count; i++) { + int cx = (int)points[i].pos.x / CHUNK_SIZE; + int cy = (int)points[i].pos.y / CHUNK_SIZE; + w->chunks[cy * w->chunks_x + cx].count++; + } + + /* Allocate all chunks' data */ + for(int i = 0; i < w->chunks_x * w->chunks_y; i++) { + if(w->chunks[i].count == 0) + continue; + w->chunks[i].points = (struct ref_point *)malloc( + w->chunks[i].count * sizeof(struct ref_point)); + if(!w->chunks[i].points) goto fail; + } + + /* Do a second pass to actually fill the chunks */ + s = w->chunks_x * w->chunks_y * sizeof(int); + js = (int *)malloc(s); + if(!js) goto fail; + for(int i = 0; i < w->chunks_x * w->chunks_y; i++) { + js[i] = 0; + } + + for(int i = 0; i < count; i++) { + int cx = (int)points[i].pos.x / CHUNK_SIZE; + int cy = (int)points[i].pos.y / CHUNK_SIZE; + int j = cy * w->chunks_x + cx; + w->chunks[j].points[js[j]++] = points[i]; + } + + free(js); + return w; + +fail: + world_destroy(w); + return NULL; +} + +void world_destroy(struct world *w) +{ + if(!w) return; + + if(w->chunks) { + for(int i = 0; i < w->chunks_x * w->chunks_y; i++) + free(w->chunks[i].points); + free(w->chunks); + } + + free(w); +} + +struct ref_point *world_select(struct world const *world, vec3 position, + GUNUSED vec3 forward, int *size, int extra_size) +{ + int cx1 = (int)position.x / CHUNK_SIZE; + int cy1 = (int)position.y / CHUNK_SIZE; + +// int fx = (int)forward.x >= 0 ? +1 : -1; +// int fy = (int)forward.y >= 0 ? +1 : -1; + + int chunks[9]; + int chunk_count = 0; + + for(int dy = -1; dy <= +1; dy++) + for(int dx = -1; dx <= +1; dx++) { + + int cy = cy1 + dy; + int cx = cx1 + dx; + + if(cx < 0) + cx = world->chunks_x - 1; + if(cx >= world->chunks_x) + cx = 0; + if(cy < 0) + cy = world->chunks_y - 1; + if(cy >= world->chunks_y) + cy = 0; + + chunks[chunk_count++] = cy * world->chunks_x + cx; + } + + /* Count the points to be allocated */ + int total_point_count = 0; + for(int i = 0; i < chunk_count; i++) + total_point_count += world->chunks[chunks[i]].count; + + /* Allocate the array */ + struct ref_point *output = (struct ref_point *)malloc( + (total_point_count + extra_size) * sizeof *output); + + /* Fill the points in selected chunks. While doing this, we also rotate the + points. The player is constrained to be within the world boundary, but + it can still be on the edge and look outside. In this situation, we + rotate the points to give the illusion that the world wraps around. */ + num world_size_x14 = num(world->w / 4); + num world_size_x34 = num(3 * world->w / 4); + num world_size_y14 = num(world->h / 4); + num world_size_y34 = num(3 * world->h / 4); + + /* This simple test only works if the map is at least 4x as large as + the depth of view of the plane, (here 256 > 4×50). */ + num wrap_x_minus = (position.x < world_size_x14) + ? world_size_x34 : num(world->w); + num wrap_x_plus = (position.x > world_size_x34) + ? world_size_x14 : num(0); + num wrap_y_minus = (position.y < world_size_y14) + ? world_size_y34 : num(world->h); + num wrap_y_plus = (position.y > world_size_y34) + ? world_size_y14 : num(0); + + int k = 0; + for(int i = 0; i < chunk_count; i++) { + struct chunk *c = &world->chunks[chunks[i]]; + for(int j = 0; j < c->count; j++) { + struct ref_point p = c->points[j]; + + if(p.pos.x < wrap_x_plus) + p.pos.x += num(world->w); + else if(p.pos.x > wrap_x_minus) + p.pos.x -= num(world->w); + if(p.pos.y < wrap_y_plus) + p.pos.y += num(world->h); + else if(p.pos.y > wrap_y_minus) + p.pos.y -= num(world->h); + + output[k++] = p; + } + } + + *size = total_point_count; + return output; +}