sprite rework
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled

This commit is contained in:
2025-05-04 11:28:45 -05:00
parent 446ad080e1
commit a85b1873dd
8 changed files with 190 additions and 171 deletions

1
.gitignore vendored
View File

@@ -29,3 +29,4 @@ game.zip
icon.ico
steam/
subprojects/*/
build_dbg/

View File

@@ -1,65 +1,107 @@
/* Sprite façade (JS) ↔ struct sprite (C)
* The C side exposes per-field getters/setters plus
* `set_affine(m)` which (re)calculates the 2×2 matrix from
* scale / skew / rotation.
* We mirror every exposed field and make sure the C
* matrix is refreshed whenever one of the three factors
* changes.
*/
var sprite = {}
var graphics = use('graphics')
var render = use('render')
var draw2d = use('draw2d')
var render = use('render')
var draw2d = use('draw2d')
var ursprite = {}
var SPRITE = Symbol() /* raw C sprite */
var POS = Symbol() /* cached JS copies of simple data */
var ROT = Symbol()
var SCALE = Symbol()
var SKEW = Symbol()
var CENTER = Symbol()
var COLOR = Symbol()
ursprite.move = function move(mv)
{
this.rect.x += mv.x
this.rect.y += mv.y
var ursprite = {
get pos() { return this[POS] },
set pos(v) {
this[POS] = v
this[SPRITE].pos = v
},
get center() { return this[CENTER] },
set center(v){
this[CENTER] = v
this[SPRITE].center = v
},
get rotation() { return this[ROT] },
set rotation(t){
this[ROT] = t
this[SPRITE].rotation = t * 2*Math.PI /* C expects radians */
this[SPRITE].set_affine()
},
get scale() { return this[SCALE] },
set scale(v){
this[SCALE] = v
this[SPRITE].scale = v
this[SPRITE].set_affine()
},
get skew() { return this[SKEW] },
set skew(v){
this[SKEW] = v
this[SPRITE].skew = v
this[SPRITE].set_affine()
},
get layer() { return this[SPRITE].layer },
set layer(n){ this[SPRITE].layer = n },
get color() { return this[COLOR] },
set color(v){
this[COLOR] = v
this[SPRITE].color = v
},
move(mv){
this.pos = {x:this.pos.x+mv.x, y:this.pos.y+mv.y}
},
moveto(p){
this.pos = p
}
}
ursprite.moveto = function moveto(pos)
{
this.rect.x = pos.x
this.rect.y = pos.y
}
var _sprites = []
ursprite.draw = function draw()
{
draw2d.image(this.image, this.rect, 0, this.anchor)
}
sprite.create = function(image, pos, anchor=[0,0], layer=0, props={}) {
var sp = Object.create(ursprite)
var raw = graphics.make_sprite()
var sprites = []
sp[SPRITE] = raw
sprite.create = function(image, pos, anchor = [0,0], layer = 0, props = {})
{
var sp = Object.create(ursprite)
sp.pos = pos
sp.rotation = 0
sp.scale = [1,1]
sp.skew = [0,0]
sp.center = anchor
sp.color = [1,1,1,1]
var image = graphics.texture(image)
sp.image = image
sp.rect = {x:pos.x, y:pos.y, width: image.texture.width, height: image.texture.height}
sp.layer = layer
sp.props = props
sp.anchor = anchor
var tex = graphics.texture(image)
raw.set_image(tex)
sp.image = tex
sprites.push(sp)
sp.layer = layer
sp.props = props
sp.anchor = anchor
_sprites.push(sp)
return sp
}
sprite.forEach = function(fn)
{
for (var sp of sprites)
fn(sp)
}
sprite.values = function()
{
return sprites.slice()
}
sprite.geometry = function()
{
return graphics.make_sprite_mesh(sprites)
}
sprite.queue = function()
{
return graphics.make_sprite_queue(sprites)
}
sprite.forEach = fn => { for (let s of _sprites) fn(s) }
sprite.values = () => _sprites.slice()
sprite.geometry= () => graphics.make_sprite_mesh(_sprites)
sprite.queue = () => graphics.make_sprite_queue(_sprites)
return sprite

View File

@@ -3034,8 +3034,6 @@ JSC_CCALL(renderer_coords,
JSC_CCALL(renderer_camera,
renderer_ctx *ctx = js2renderer_ctx(js,self);
JSValue camera = argv[0];
ctx->cam = camera_globals(js, argv[0]);
)
@@ -3115,25 +3113,15 @@ JSC_CCALL(renderer_make_sprite_mesh,
JS_SetProperty(js, ret, count, number2js(js, count));
)
JSC_CCALL(renderer_texture_9grid,
renderer_ctx *ctx = js2renderer_ctx(js, self);
SDL_Renderer *r = ctx->sdl;
SDL_Texture *tex = js2SDL_Texture(js, argv[0]);
rect src = js2rect(js, argv[1]);
lrtb extents = js2lrtb(js, argv[2]);
float scale = 1;
rect dst = js2rect(js,argv[3]);
dst = renderer_worldrect_to_screen(ctx, dst);
if (!SDL_RenderTexture9Grid(r, tex, &src,
extents.l, extents.r, extents.t, extents.b,
scale, &dst))
return JS_ThrowReferenceError(js, "SDL_RenderTexture9Grid: %s", SDL_GetError());
)
static sprite js_getsprite(JSContext *js, JSValue sp)
{
sprite *s = js2sprite(js, sp);
if (s)
return *s;
sprite pp = {0};
return pp;
}
JSC_CCALL(renderer_texture,
renderer_ctx *ctx = js2renderer_ctx(js, self);
@@ -3156,24 +3144,26 @@ JSC_CCALL(renderer_texture,
JSC_CCALL(renderer_sprite,
renderer_ctx *ctx = js2renderer_ctx(js, self);
sprite *sp = js2sprite(js,argv[0]);
sprite sp = js_getsprite(js, argv[0]);
SDL_Texture *tex;
JS_GETATOM(js, tex, sp->image, texture, SDL_Texture);
JS_GETATOM(js, tex, sp.image, texture, SDL_Texture);
// rect dst = renderer_worldrect_to_screen(ctx, sp->affine);
rect dst = sp->affine;
rect uv = {x:0,y:0,w:48,h:24};
SDL_RenderTexture(ctx->sdl, tex, &uv, &dst);
)
float w = sp.uv.w, h = sp.uv.h;
JSC_CCALL(renderer_slice9,
renderer_ctx *ctx = js2renderer_ctx(js, self);
SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
rect src = js2rect(js, argv[1]);
lrtb exts = js2lrtb(js, argv[2]);
rect dst = renderer_worldrect_to_screen(ctx, js2rect(js, argv[3]));
SDL_RenderTexture9Grid(ctx->sdl, tex, &src, exts.l, exts.r, exts.t, exts.b, 0.0f, &dst);
HMM_Vec2 tl_local = { -sp.center.X, -sp.center.Y };
HMM_Vec2 tr_local = { -sp.center.X + w, -sp.center.Y };
HMM_Vec2 bl_local = { -sp.center.X, -sp.center.Y + h };
HMM_Vec2 world_tl = HMM_AddV2(sp.pos, HMM_MulM2V2(sp.affine, tl_local));
HMM_Vec2 world_tr = HMM_AddV2(sp.pos, HMM_MulM2V2(sp.affine, tr_local));
HMM_Vec2 world_bl = HMM_AddV2(sp.pos, HMM_MulM2V2(sp.affine, bl_local));
SDL_FPoint origin = renderer_world_to_screen(ctx, world_tl);
SDL_FPoint right = renderer_world_to_screen(ctx, world_tr);
SDL_FPoint down = renderer_world_to_screen(ctx, world_bl);
if (!SDL_RenderTextureAffine(ctx->sdl, tex, &sp.uv, &origin, &right, &down))
return JS_ThrowInternalError(js, "Render sprite error: %s", SDL_GetError());
)
static const JSCFunctionListEntry js_renderer_ctx_funcs[] = {
@@ -3189,7 +3179,6 @@ static const JSCFunctionListEntry js_renderer_ctx_funcs[] = {
MIST_FUNC_DEF(renderer, sprite, 1),
MIST_FUNC_DEF(renderer, load_texture, 1),
MIST_FUNC_DEF(renderer, get_image, 1),
MIST_FUNC_DEF(renderer, slice9, 4),
MIST_FUNC_DEF(renderer, scale, 1),
MIST_FUNC_DEF(renderer, logical_size,1),
@@ -3263,11 +3252,11 @@ static SDL_AudioSpec js2audiospec(JSContext *js, JSValue obj)
JS_FreeValue(js, v);
v = JS_GetPropertyStr(js, obj, "channels");
if (!JS_IsUndefined(v)) JS_ToUint32(js, &spec.channels, v);
if (!JS_IsUndefined(v)) JS_ToInt32(js, &spec.channels, v);
JS_FreeValue(js, v);
v = JS_GetPropertyStr(js, obj, "samplerate");
if (!JS_IsUndefined(v)) JS_ToUint32(js, &spec.freq, v);
if (!JS_IsUndefined(v)) JS_ToInt32(js, &spec.freq, v);
JS_FreeValue(js, v);
return spec;
@@ -3294,7 +3283,7 @@ JSC_CCALL(sdl_audio_devices,
)
JSC_CCALL(sdl_audio_open_stream,
char *type = JS_IsString(argv[0]) ? JS_ToCString(js, argv[0]) : NULL;
const char *type = JS_IsString(argv[0]) ? JS_ToCString(js, argv[0]) : NULL;
SDL_AudioDeviceID devid = !strcmp(type, "capture") ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING : SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
if (type)
@@ -4042,11 +4031,11 @@ typedef struct {
static inline int sprite_compare(const sprite *a, const sprite *b) {
if (a->layer != b->layer) return a->layer - b->layer;
if (a->affine.y != b->affine.y)
return (b->affine.y - a->affine.y);
if (a->pos.Y != b->pos.Y)
return (b->pos.Y - a->pos.Y);
if (a->tex != b->tex)
return (a->tex < b->tex) ? -1 : 1;
if (JS_VALUE_GET_PTR(a->image) != JS_VALUE_GET_PTR(b->image))
return JS_VALUE_GET_PTR(a->image) < JS_VALUE_GET_PTR(b->image) ? -1 : 1;
return 0;
}
@@ -4087,28 +4076,35 @@ void better_sort_sprites(sprite *arr, size_t length) {
int sort_sprite_backtofront(const sprite *a, const sprite *b)
{
if (a->layer != b->layer) return a->layer - b->layer;
if (a->tex != b->tex) return ((uintptr_t)a->tex < (uintptr_t)b->tex) ? -1 : 1;
if (JS_VALUE_GET_PTR(a->image) != JS_VALUE_GET_PTR(b->image))
return JS_VALUE_GET_PTR(a->image) < JS_VALUE_GET_PTR(b->image) ? -1 : 1;
return 0;
}
int sort_sprite_fronttoback(const sprite *a, const sprite *b)
{
if (a->layer != b->layer) return b->layer - a->layer;
if (a->tex != b->tex) return ((uintptr_t)a->tex < (uintptr_t)b->tex) ? -1 : 1;
if (JS_VALUE_GET_PTR(a->image) != JS_VALUE_GET_PTR(b->image))
return JS_VALUE_GET_PTR(a->image) < JS_VALUE_GET_PTR(b->image) ? -1 : 1;
return 0;
}
int sort_sprite_texture(const sprite *a, const sprite *b)
{
if (a->tex != b->tex) return ((uintptr_t)a->tex < (uintptr_t)b->tex) ? -1 : 1;
if (JS_VALUE_GET_PTR(a->image) != JS_VALUE_GET_PTR(b->image))
return JS_VALUE_GET_PTR(a->image) < JS_VALUE_GET_PTR(b->image) ? -1 : 1;
return 0;
}
int sort_sprite(const sprite *a, const sprite *b)
{
if (a->layer != b->layer) return a->layer - b->layer;
if (a->affine.y != b->affine.y) return b->affine.y - a->affine.y;
if (a->tex != b->tex) return ((uintptr_t)a->tex < (uintptr_t)b->tex) ? -1 : 1;
if (a->pos.Y != b->pos.Y)
return (b->pos.Y - a->pos.Y);
if (JS_VALUE_GET_PTR(a->image) != JS_VALUE_GET_PTR(b->image))
return JS_VALUE_GET_PTR(a->image) < JS_VALUE_GET_PTR(b->image) ? -1 : 1;
return 0;
}
@@ -4139,7 +4135,6 @@ JSC_CCALL(gpu_make_sprite_queue,
sprite *sprites = NULL;
size_t quads = 0;
int needfree = 1;
int sort = js2number(js,argv[3]);
if (JS_IsArrayBuffer(js, argv[0])) {
// test for fastest
@@ -4162,26 +4157,26 @@ JSC_CCALL(gpu_make_sprite_queue,
}
else {
sprite sp = {0};
JS_GETATOM(js,sp.affine, sub, rect, rect)
JS_GETATOM(js, sp.pos, sub, pos, vec2)
JS_GETATOM(js, sp.center, sub, center, vec2)
JS_GETATOM(js, sp.skew, sub, skew, vec2)
JS_GETATOM(js, sp.scale, sub, scale, vec2)
JS_GETATOM(js,sp.color,sub,color,color)
JS_GETATOM(js,sp.layer,sub,layer,number)
JS_GETATOM(js,sp.uv,sub,src,rect)
sp.image = JS_GetProperty(js,sub,image);
JS_GETATOM(js,sp.tex,sp.image,texture,SDL_GPUTexture)
arrput(sprites,sp);
}
JS_FreeValue(js, sub);
}
}
if (sort) qsort(sprites, quads, sizeof(sprite), sort_sprite);
// else qsort(sprites, quads, sizeof(sprite), sort_sprite_texture);
qsort(sprites, quads, sizeof(sprite), sort_sprite);
struct quad_buffers buffers = quad_buffers_new(quads*4);
for (int i = 0; i < quads; i++) {
rect pr = sprites[i].affine;
rect pr = {0};
rect uv = sprites[i].uv;
HMM_Vec4 c = sprites[i].color;
@@ -6826,36 +6821,6 @@ JSC_CCALL(os_make_rtree,
return rtree2js(js,tree);
)
JSC_CCALL(os_rects_to_sprites,
ret = JS_NewArray(js);
JSValue image = argv[0];
JSValue jstex = JS_GetProperty(js,image,texture);
double w, h = 0;
JS_GETATOM(js,w,jstex,width,number)
JS_GETATOM(js,h,jstex,height,number)
SDL_GPUTexture *tex = js2SDL_GPUTexture(js,jstex);
JS_FreeValue(js,jstex);
JSValue rects = argv[1];
rect uv;
JS_GETATOM(js,uv,image,rect,rect)
ret = JS_NewArray(js);
int n = js_arrlen(js,rects);
for (int i = 0; i < n; i++) {
JSValue sub = JS_GetPropertyUint32(js,rects,i);
sprite *s = make_sprite();
s->affine = js2rect(js,sub);
s->affine.w = w;
s->affine.h = h;
s->image = JS_DupValue(js,image);
s->tex = tex;
s->uv = uv;
JS_SetPropertyUint32(js,ret,i,sprite2js(js,s));
JS_FreeValue(js,sub);
}
)
JSC_CCALL(os_on,
prosperon_rt *rt = JS_GetContextOpaque(js);
JS_FreeValue(js, rt->on_exception);
@@ -6995,7 +6960,7 @@ JSC_CCALL(os_createactor,
for (int i = 0; i < margc; i++) {
JSValue val = JS_GetPropertyUint32(js, argv[0], i);
char *cstr = JS_ToCString(js,val);
const char *cstr = JS_ToCString(js,val);
margv[i] = strdup(cstr);
JS_FreeCString(js,cstr);
JS_FreeValue(js,val);
@@ -7018,7 +6983,7 @@ JSC_CCALL(os_mailbox_push,
void *data = value2wota(js, argv[1], JS_UNDEFINED);
char *err = send_message(id, data);
const char *err = send_message(id, data);
if (err) {
free(data);
return JS_ThrowInternalError(js, "Could not send message: %s", err);
@@ -7027,8 +6992,8 @@ JSC_CCALL(os_mailbox_push,
JSC_CCALL(os_register_actor,
prosperon_rt *rt = JS_GetContextOpaque(js);
char *id = JS_ToCString(js, argv[0]);
char *err = register_actor(id, rt, JS_ToBool(js, argv[2]));
const char *id = JS_ToCString(js, argv[0]);
const char *err = register_actor(id, rt, JS_ToBool(js, argv[2]));
if (err) return JS_ThrowInternalError(js, "Could not register actor: %s", err);
rt->message_handle = JS_DupValue(js, argv[1]);
rt->context = js;
@@ -7184,7 +7149,6 @@ static const JSCFunctionListEntry js_graphics_funcs[] = {
MIST_FUNC_DEF(os, make_gif, 1),
MIST_FUNC_DEF(os, make_aseprite, 1),
MIST_FUNC_DEF(os, cull_sprites, 2),
MIST_FUNC_DEF(os, rects_to_sprites,2),
MIST_FUNC_DEF(os, make_surface, 1),
MIST_FUNC_DEF(os, make_cursor, 1),
MIST_FUNC_DEF(os, make_font, 2),
@@ -7390,19 +7354,17 @@ static const JSCFunctionListEntry js_rtree_funcs[] = {
MIST_FUNC_DEF(rtree,values,0),
};
JSC_GETSET(sprite, pos, vec2)
JSC_GETSET(sprite, center, vec2)
JSC_GETSET(sprite, layer, number)
JSC_GETSET(sprite, color, color)
JSC_GETSET(sprite, skew, vec2)
JSC_GETSET(sprite, scale, vec2)
JSC_GETSET(sprite, rotation, number)
JSC_CCALL(sprite_set_affine,
sprite *sp = js2sprite(js,self);
transform *t = js2transform(js,argv[0]);
if (t)
sp->affine = transform2rect(t);
)
JSC_CCALL(sprite_set_rect,
sprite *sp = js2sprite(js,self);
sp->affine = js2rect(js,argv[0]);
sprite_apply(sp);
)
JSC_CCALL(sprite_set_image,
@@ -7413,13 +7375,15 @@ JSC_CCALL(sprite_set_image,
if (JS_IsUndefined(sp->image)) return JS_UNDEFINED;
JSValue img = sp->image;
JS_GETATOM(js, sp->uv, img, rect, rect)
JS_GETATOM(js, sp->tex, img, texture, SDL_GPUTexture);
)
static const JSCFunctionListEntry js_sprite_funcs[] = {
MIST_FUNC_DEF(sprite, set_affine, 1),
MIST_FUNC_DEF(sprite, set_rect, 1),
MIST_FUNC_DEF(sprite, set_affine, 0),
MIST_FUNC_DEF(sprite, set_image, 1),
CGETSET_ADD(sprite, skew),
CGETSET_ADD(sprite, rotation),
CGETSET_ADD(sprite, pos),
CGETSET_ADD(sprite, center),
CGETSET_ADD(sprite, layer),
CGETSET_ADD(sprite, color),
};
@@ -7563,8 +7527,8 @@ void ffi_load(JSContext *js)
JS_SetPropertyUint32(js,args, i, JS_NewString(js,rt->cmd.argv[i]));
JS_SetPropertyStr(js,prosp,"argv", args);
JS_SetPropertyStr(js,prosp, "version", JS_NewString(js,PROSPERON_VERSION));
JS_SetPropertyStr(js,prosp,"revision",JS_NewString(js,PROSPERON_COMMIT));
//JS_SetPropertyStr(js,prosp, "version", JS_NewString(js,PROSPERON_VERSION));
//JS_SetPropertyStr(js,prosp,"revision",JS_NewString(js,PROSPERON_COMMIT));
JS_SetPropertyStr(js,prosp,"engine_start", JS_NewCFunction(js,js_os_engine_start, "engine_start", 1));
JS_FreeValue(js,globalThis);

View File

@@ -238,7 +238,7 @@ prosperon_rt *get_actor(char *id)
return actor;
}
char *register_actor(char *id, prosperon_rt *actor, int mainthread)
const char *register_actor(const char *id, prosperon_rt *actor, int mainthread)
{
if (shgeti(actors, id) != -1) return "Actor with given ID already exists.";
actor->main_thread_only = mainthread;
@@ -249,7 +249,7 @@ char *register_actor(char *id, prosperon_rt *actor, int mainthread)
return NULL;
}
char *send_message(char *id, void *msg)
const char *send_message(const char *id, void *msg)
{
prosperon_rt *target = get_actor(id);
if (!target) return "Could not get actor from id.";
@@ -1425,7 +1425,7 @@ int main(int argc, char **argv)
return 0;
}
int actor_exists(char *id)
int actor_exists(const char *id)
{
int idx = shgeti(actors,id);
if (idx == -1)

View File

@@ -71,9 +71,9 @@ extern SDL_ThreadID main_thread;
extern SDL_TLSID prosperon_id;
prosperon_rt *create_actor(int argc, char **argv);
char *register_actor(char *id, prosperon_rt *actor, int mainthread);
const char *register_actor(const char *id, prosperon_rt *actor, int mainthread);
void actor_free(prosperon_rt *actor);
char *send_message(char *id, void *msg);
const char *send_message(const char *id, void *msg);
Uint32 actor_timer_cb(prosperon_rt *actor, SDL_TimerID id, Uint32 interval);
JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv);
JSValue js_actor_removetimer(JSContext *js, JSValue self, int argc, JSValue *argv);
@@ -81,7 +81,7 @@ void script_startup(prosperon_rt *rt);
void script_evalf(JSContext *js, const char *format, ...);
JSValue script_eval(JSContext *js, const char *file, const char *script);
int uncaught_exception(JSContext *js, JSValue v);
int actor_exists(char *id);
int actor_exists(const char *id);
void set_actor_state(prosperon_rt *actor);
int prosperon_mount_core(void);

View File

@@ -371,7 +371,7 @@ static JSClassDef enet_peer_class = {
JSValue js_enet_resolve_hostname(JSContext *js, JSValue self, int argc, JSValue *argv)
{
// TODO: implement
char *hostname = JS_ToCString(js, argv[0]);
const char *hostname = JS_ToCString(js, argv[0]);
JS_FreeCString(js, hostname);
return JS_UNDEFINED;
}

View File

@@ -1,17 +1,8 @@
#include "sprite.h"
static sprite model = {
.affine = {.x = 0, .y = 0, .w = 0, .h = 0},
.tex = NULL,
.uv = {.x = 0, .y = 0, .w = 1, .h = 1},
.layer = 0,
.color = {1, 1, 1, 1}
};
sprite *make_sprite(void)
{
sprite *sprite = malloc(sizeof(*sprite));
*sprite = model;
sprite *sprite = calloc(sizeof(*sprite),1);
sprite->image = JS_UNDEFINED;
return sprite;
}
@@ -21,3 +12,21 @@ void sprite_free(JSRuntime *rt, sprite *sprite)
JS_FreeValueRT(rt,sprite->image);
free(sprite);
}
void sprite_apply(sprite *sp)
{
float rot = sp->rotation;
HMM_Vec2 k = sp->skew;
HMM_Vec2 s = sp->scale;
float c = cosf(rot), si = sinf(rot);
sp->affine.Columns[0] = (HMM_Vec2){
.x = (c + k.x * si) * s.x,
.y = (k.y * c + si) * s.x
};
sp->affine.Columns[1] = (HMM_Vec2){
.x = (-si + k.x * c) * s.y,
.y = (-k.y * si + c) * s.y
};
}

View File

@@ -7,10 +7,12 @@
struct sprite{
HMM_Vec2 pos; // x,y coordinates of the sprite
HMM_Mat2 affine_m; // defines all deformation
rect affine; //
HMM_Vec2 center;
HMM_Vec2 skew;
HMM_Vec2 scale;
float rotation;
HMM_Mat2 affine;
JSValue image; // the actual texture resource - perhaps SDL_GPUTexture, or SDL_Texture, or something else ...
SDL_GPUTexture *tex;
rect uv; // pixel coordinates of the sprite on its image
int layer; // layer this sprite draws onto
HMM_Vec4 color; // color in 0-1f
@@ -20,5 +22,6 @@ typedef struct sprite sprite;
sprite *make_sprite(void);
void sprite_free(JSRuntime *rt, sprite *sprite);
void sprite_apply(sprite *sprite);
#endif