6 n__allegro__4_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00

src/n_allegro_4.c

Allegro 4 compatibility layer on top of Allegro 5. More...

Structures

Name
struct SAMPLE
a sound sample
struct MIDI
a MIDI song
struct LexMenu
an INI driven menu
struct LEX_ITEM
struct FONT
a font: the built in one, or a TTF font loaded with load_ttf_font()
struct A4_TIMER
struct A4_BMP

Types

Name
typedef int(*)(void *, int, void *, int, int, void *, int, int) fs_write_float_f
typedef int(*)(void *, const char *, int) fs_sfload_f
typedef int(*)(void *, const char *, const char *) fs_settings_setstr_f
typedef int(*)(void *, const char *, double) fs_settings_setnum_f
typedef void ()(int, void *, void *) fs_set_log_function_f
typedef int(*)(void *, int) fs_player_set_loop_f
typedef int(*)(void *) fs_player_int_f
typedef int(*)(void *, const char *) fs_player_add_f
typedef void ()(void *) fs_new_synth_f
typedef void ()(void) fs_new_settings_f
typedef void ()(void *) fs_new_player_f
typedef void(*)(void *) fs_delete_f

Functions

Name
BITMAP * _a4_wrap(ALLEGRO_BITMAP * abmp)
void _a4_vertex(ALLEGRO_VERTEX * v, float x, float y, ALLEGRO_COLOR c)
void _a4_unlock(BITMAP * b)
void * _a4_timer_proc(ALLEGRO_THREAD * thr, void * arg)
void _a4_text(BITMAP * bmp, const FONT * f, const char * s, int x, int y, int color, int bg, int align)
int _a4_target(BITMAP * b)
int _a4_take_click(int * x, int * y)
char * _a4_strndup(const char * s, size_t n)
void _a4_sprite(BITMAP * bmp, BITMAP * sprite, int x, int y, int flags)
ALLEGRO_BITMAP * _a4_source(BITMAP * b, int masked)
const char * _a4_soundfont(void )
void _a4_set_blend(int blend)
void _a4_rotated(BITMAP * bmp, BITMAP * sprite, float x, float y, float cx, float cy, fixed angle, fixed scale)
ALLEGRO_COLOR _a4_rgba(int color, int alpha)
char * _a4_resolve(const char * path)
ALLEGRO_VERTEX * _a4_reserve(BITMAP * b, int blend, int n)
void _a4_quad(BITMAP * b, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, int color)
ALLEGRO_COLOR _a4_prim_color(int color)
ALLEGRO_COLOR _a4_prim(int color, int * blend)
void _a4_present(void )
float _a4_pan(int pan)
void _a4_midi_stop_locked(void )
void _a4_midi_shutdown(void )
void * _a4_midi_proc(ALLEGRO_THREAD * thr, void * arg)
int _a4_midi_init(void )
ALLEGRO_LOCKED_REGION * _a4_lock(BITMAP * b)
void _a4_item_rect(const LexMenu * menu, const BITMAP * buf, int i, float * x, float * y, float * w, float * h)
int _a4_item_at(const LexMenu * menu, const BITMAP * buf, int mx, int my)
void * _a4_input_proc(ALLEGRO_THREAD * thr, void * arg)
void _a4_input_handle(const ALLEGRO_EVENT * ev)
BITMAP * _a4_highlight(BITMAP * src)
float _a4_gain(int vol)
ALLEGRO_FONT * _a4_font(const FONT * f)
void _a4_flush(void )
int _a4_fluidsynth_sym(N_MODULE * lib, const char * name, void * fptr, size_t fsize)
N_MODULE * _a4_fluidsynth_open(void )
void _a4_draw_region(BITMAP * source, BITMAP * dest, int masked, float sx, float sy, float sw, float sh, float dx, float dy, float dw, float dh, int flags)
void _a4_done(const BITMAP * b)
void _a4_destroy_display(void )
void _a4_compute_scale(void )
int _a4_color_from_pixel(uint32_t p)
void _a4_box(BITMAP * b, float l, float t, float r, float bt, int color)
void _a4_blend_copy(void )
void _a4_blend_alpha(void )
int _a4_align(const char * v, int is_h)

Attributes

Name
int a4_trans_alpha
A4_TIMER[16] a4_timers
ALLEGRO_THREAD * a4_timer_thread
ALLEGRO_MUTEX * a4_timer_mutex
int a4_sound_ok
int a4_screen_dirty
float a4_scale
float a4_off_y
float a4_off_x
int a4_midi_volume
int a4_mickey_y
int a4_mickey_x
int a4_keybuf_start
int a4_keybuf_count
int[64] a4_keybuf
ALLEGRO_THREAD * a4_input_thread
ALLEGRO_EVENT_QUEUE * a4_input_queue
ALLEGRO_MUTEX * a4_input_mutex
int a4_inited
int a4_gl_mode
struct @346262156126311217342151060122117020361320217156 a4_fs
int a4_draw_mode
ALLEGRO_DISPLAY * a4_display
int a4_digi_volume
int a4_cursor_shown
volatile int a4_close_requested
void(*)(void) a4_close_cb
int a4_click_y
int a4_click_x
int a4_click_pending
BITMAP * a4_batch_target
int a4_batch_n
int a4_batch_blend
ALLEGRO_VERTEX[6144] a4_batch
int a4_alpha_blender

Defines

Name
ALLEGRO_UNSTABLE
A4_TEXTPRINTF(align)
A4_SYM(field, name)
A4_SAMPLE_IDS
A4_MIDI_RATE
A4_MIDI_FRAG
A4_MAX_TIMERS
A4_MASK_COLOR
A4_KEYBUF_SIZE
A4_BLEND_FADE
A4_BLEND_COPY
A4_BLEND_ALPHA
A4_BATCH_MAX
A4P(b)

Detailed Description

Allegro 4 compatibility layer on top of Allegro 5.

Author: Castagnier Mickael

Version: 1.0

Date: 08/10/2026

Types Documentation

typedef fs_write_float_f

typedef int(* fs_write_float_f) (void *, int, void *, int, int, void *, int, int);

typedef fs_sfload_f

typedef int(* fs_sfload_f) (void *, const char *, int);

typedef fs_settings_setstr_f

typedef int(* fs_settings_setstr_f) (void *, const char *, const char *);

typedef fs_settings_setnum_f

typedef int(* fs_settings_setnum_f) (void *, const char *, double);

typedef fs_set_log_function_f

typedef void *(* fs_set_log_function_f) (int, void *, void *);

typedef fs_player_set_loop_f

typedef int(* fs_player_set_loop_f) (void *, int);

typedef fs_player_int_f

typedef int(* fs_player_int_f) (void *);

typedef fs_player_add_f

typedef int(* fs_player_add_f) (void *, const char *);

typedef fs_new_synth_f

typedef void *(* fs_new_synth_f) (void *);

typedef fs_new_settings_f

typedef void *(* fs_new_settings_f) (void);

typedef fs_new_player_f

typedef void *(* fs_new_player_f) (void *);

typedef fs_delete_f

typedef void(* fs_delete_f) (void *);

Functions Documentation

function _a4_wrap

static BITMAP * _a4_wrap(
    ALLEGRO_BITMAP * abmp
)

function _a4_vertex

static void _a4_vertex(
    ALLEGRO_VERTEX * v,
    float x,
    float y,
    ALLEGRO_COLOR c
)

function _a4_unlock

static void _a4_unlock(
    BITMAP * b
)

function _a4_timer_proc

static void * _a4_timer_proc(
    ALLEGRO_THREAD * thr,
    void * arg
)

function _a4_text

static void _a4_text(
    BITMAP * bmp,
    const FONT * f,
    const char * s,
    int x,
    int y,
    int color,
    int bg,
    int align
)

function _a4_target

static int _a4_target(
    BITMAP * b
)

function _a4_take_click

static int _a4_take_click(
    int * x,
    int * y
)

function _a4_strndup

static char * _a4_strndup(
    const char * s,
    size_t n
)

function _a4_sprite

static void _a4_sprite(
    BITMAP * bmp,
    BITMAP * sprite,
    int x,
    int y,
    int flags
)

function _a4_source

static ALLEGRO_BITMAP * _a4_source(
    BITMAP * b,
    int masked
)

function _a4_soundfont

static const char * _a4_soundfont(
    void 
)

function _a4_set_blend

static void _a4_set_blend(
    int blend
)

function _a4_rotated

static void _a4_rotated(
    BITMAP * bmp,
    BITMAP * sprite,
    float x,
    float y,
    float cx,
    float cy,
    fixed angle,
    fixed scale
)

function _a4_rgba

static ALLEGRO_COLOR _a4_rgba(
    int color,
    int alpha
)

function _a4_resolve

static char * _a4_resolve(
    const char * path
)

function _a4_reserve

static ALLEGRO_VERTEX * _a4_reserve(
    BITMAP * b,
    int blend,
    int n
)

function _a4_quad

static void _a4_quad(
    BITMAP * b,
    float x1,
    float y1,
    float x2,
    float y2,
    float x3,
    float y3,
    float x4,
    float y4,
    int color
)

function _a4_prim_color

static ALLEGRO_COLOR _a4_prim_color(
    int color
)

function _a4_prim

static ALLEGRO_COLOR _a4_prim(
    int color,
    int * blend
)

function _a4_present

static void _a4_present(
    void 
)

function _a4_pan

static float _a4_pan(
    int pan
)

function _a4_midi_stop_locked

static void _a4_midi_stop_locked(
    void 
)

function _a4_midi_shutdown

static void _a4_midi_shutdown(
    void 
)

function _a4_midi_proc

static void * _a4_midi_proc(
    ALLEGRO_THREAD * thr,
    void * arg
)

function _a4_midi_init

static int _a4_midi_init(
    void 
)

function _a4_lock

static ALLEGRO_LOCKED_REGION * _a4_lock(
    BITMAP * b
)

function _a4_item_rect

static void _a4_item_rect(
    const LexMenu * menu,
    const BITMAP * buf,
    int i,
    float * x,
    float * y,
    float * w,
    float * h
)

function _a4_item_at

static int _a4_item_at(
    const LexMenu * menu,
    const BITMAP * buf,
    int mx,
    int my
)

function _a4_input_proc

static void * _a4_input_proc(
    ALLEGRO_THREAD * thr,
    void * arg
)

function _a4_input_handle

static void _a4_input_handle(
    const ALLEGRO_EVENT * ev
)

function _a4_highlight

static BITMAP * _a4_highlight(
    BITMAP * src
)

function _a4_gain

static float _a4_gain(
    int vol
)

function _a4_font

static ALLEGRO_FONT * _a4_font(
    const FONT * f
)

function _a4_flush

static void _a4_flush(
    void 
)

function _a4_fluidsynth_sym

static int _a4_fluidsynth_sym(
    N_MODULE * lib,
    const char * name,
    void * fptr,
    size_t fsize
)

function _a4_fluidsynth_open

static N_MODULE * _a4_fluidsynth_open(
    void 
)

function _a4_draw_region

static void _a4_draw_region(
    BITMAP * source,
    BITMAP * dest,
    int masked,
    float sx,
    float sy,
    float sw,
    float sh,
    float dx,
    float dy,
    float dw,
    float dh,
    int flags
)

function _a4_done

static void _a4_done(
    const BITMAP * b
)

function _a4_destroy_display

static void _a4_destroy_display(
    void 
)

function _a4_compute_scale

static void _a4_compute_scale(
    void 
)

function _a4_color_from_pixel

static int _a4_color_from_pixel(
    uint32_t p
)

function _a4_box

static void _a4_box(
    BITMAP * b,
    float l,
    float t,
    float r,
    float bt,
    int color
)

function _a4_blend_copy

static void _a4_blend_copy(
    void 
)

function _a4_blend_alpha

static void _a4_blend_alpha(
    void 
)

function _a4_align

static int _a4_align(
    const char * v,
    int is_h
)

Attributes Documentation

variable a4_trans_alpha

static int a4_trans_alpha = 255;

variable a4_timers

static A4_TIMER[16] a4_timers;

variable a4_timer_thread

static ALLEGRO_THREAD * a4_timer_thread = NULL;

variable a4_timer_mutex

static ALLEGRO_MUTEX * a4_timer_mutex = NULL;

variable a4_sound_ok

static int a4_sound_ok = 0;

variable a4_screen_dirty

static int a4_screen_dirty = 0;

variable a4_scale

static float a4_scale = 1.0f;

variable a4_off_y

static float a4_off_y = 0.0f;

variable a4_off_x

static float a4_off_x = 0.0f;

variable a4_midi_volume

static int a4_midi_volume = 255;

variable a4_mickey_y

static int a4_mickey_y = 0;

variable a4_mickey_x

static int a4_mickey_x = 0;

variable a4_keybuf_start

static int a4_keybuf_start = 0;

variable a4_keybuf_count

static int a4_keybuf_count = 0;

variable a4_keybuf

static int[64] a4_keybuf;

variable a4_input_thread

static ALLEGRO_THREAD * a4_input_thread = NULL;

variable a4_input_queue

static ALLEGRO_EVENT_QUEUE * a4_input_queue = NULL;

variable a4_input_mutex

static ALLEGRO_MUTEX * a4_input_mutex = NULL;

variable a4_inited

static int a4_inited = 0;

variable a4_gl_mode

static int a4_gl_mode = 0;

variable a4_fs

static struct @346262156126311217342151060122117020361320217156 a4_fs;

variable a4_draw_mode

static int a4_draw_mode = 0;

variable a4_display

static ALLEGRO_DISPLAY * a4_display = NULL;

variable a4_digi_volume

static int a4_digi_volume = 255;

variable a4_cursor_shown

static int a4_cursor_shown = 0;

variable a4_close_requested

static volatile int a4_close_requested = 0;

variable a4_close_cb

static void(*)(void) a4_close_cb = NULL;

variable a4_click_y

static int a4_click_y = 0;

variable a4_click_x

static int a4_click_x = 0;

variable a4_click_pending

static int a4_click_pending = 0;

variable a4_batch_target

static BITMAP * a4_batch_target = NULL;

variable a4_batch_n

static int a4_batch_n = 0;

variable a4_batch_blend

static int a4_batch_blend = 0;

variable a4_batch

static ALLEGRO_VERTEX[6144] a4_batch;

variable a4_alpha_blender

static int a4_alpha_blender = 0;

Macros Documentation

define ALLEGRO_UNSTABLE

#define ALLEGRO_UNSTABLE 

define A4_TEXTPRINTF

#define A4_TEXTPRINTF(
    align
)
char buf[1024];                          \
va_list ap;                              \
va_start(ap, format);                    \
vsnprintf(buf, sizeof(buf), format, ap); \
va_end(ap);                              \
_a4_text(bmp, f, buf, x, y, color, bg, align);

define A4_SYM

#define A4_SYM(
    field,
    name
)
ok = ok && _a4_fluidsynth_sym(a4_fs.lib, name, &a4_fs.field, sizeof(a4_fs.field))

define A4_SAMPLE_IDS

#define A4_SAMPLE_IDS 16

define A4_MIDI_RATE

#define A4_MIDI_RATE 44100

define A4_MIDI_FRAG

#define A4_MIDI_FRAG 1024

define A4_MAX_TIMERS

#define A4_MAX_TIMERS 16

define A4_MASK_COLOR

#define A4_MASK_COLOR 0x00FF00FF

define A4_KEYBUF_SIZE

#define A4_KEYBUF_SIZE 64

define A4_BLEND_FADE

#define A4_BLEND_FADE 2

define A4_BLEND_COPY

#define A4_BLEND_COPY 0

define A4_BLEND_ALPHA

#define A4_BLEND_ALPHA 1

define A4_BATCH_MAX

#define A4_BATCH_MAX 6144

define A4P

#define A4P(
    b
)
((A4_BMP*)((b)->priv))

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */


/* al_lock_sample_id() is part of Allegro 5's unstable API */
#ifndef ALLEGRO_UNSTABLE
#define ALLEGRO_UNSTABLE
#endif

#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <math.h>

#include <allegro5/allegro.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_ttf.h>
#include <allegro5/allegro_audio.h>
#include <allegro5/allegro_acodec.h>

#include "nilorea/n_common.h"
#include "nilorea/n_log.h"
#include "nilorea/n_module.h"
#include "nilorea/n_allegro_4.h"

#ifndef __windows__
#include <dirent.h>
#include <unistd.h>
#endif

/* The KEY_* values of the header are Allegro 5's key codes. */
_Static_assert(KEY_A == ALLEGRO_KEY_A, "KEY_A");
_Static_assert(KEY_0 == ALLEGRO_KEY_0, "KEY_0");
_Static_assert(KEY_F1 == ALLEGRO_KEY_F1, "KEY_F1");
_Static_assert(KEY_ESC == ALLEGRO_KEY_ESCAPE, "KEY_ESC");
_Static_assert(KEY_BACKSPACE == ALLEGRO_KEY_BACKSPACE, "KEY_BACKSPACE");
_Static_assert(KEY_TAB == ALLEGRO_KEY_TAB, "KEY_TAB");
_Static_assert(KEY_ENTER == ALLEGRO_KEY_ENTER, "KEY_ENTER");
_Static_assert(KEY_SPACE == ALLEGRO_KEY_SPACE, "KEY_SPACE");
_Static_assert(KEY_INSERT == ALLEGRO_KEY_INSERT, "KEY_INSERT");
_Static_assert(KEY_DEL == ALLEGRO_KEY_DELETE, "KEY_DEL");
_Static_assert(KEY_HOME == ALLEGRO_KEY_HOME, "KEY_HOME");
_Static_assert(KEY_END == ALLEGRO_KEY_END, "KEY_END");
_Static_assert(KEY_PGUP == ALLEGRO_KEY_PGUP, "KEY_PGUP");
_Static_assert(KEY_PGDN == ALLEGRO_KEY_PGDN, "KEY_PGDN");
_Static_assert(KEY_LEFT == ALLEGRO_KEY_LEFT, "KEY_LEFT");
_Static_assert(KEY_RIGHT == ALLEGRO_KEY_RIGHT, "KEY_RIGHT");
_Static_assert(KEY_UP == ALLEGRO_KEY_UP, "KEY_UP");
_Static_assert(KEY_DOWN == ALLEGRO_KEY_DOWN, "KEY_DOWN");
_Static_assert(KEY_ENTER_PAD == ALLEGRO_KEY_PAD_ENTER, "KEY_ENTER_PAD");
_Static_assert(KEY_LSHIFT == ALLEGRO_KEY_LSHIFT, "KEY_LSHIFT");
_Static_assert(KEY_ALTGR == ALLEGRO_KEY_ALTGR, "KEY_ALTGR");
_Static_assert(KEY_MAX == ALLEGRO_KEY_MAX, "KEY_MAX");

/* Mask colour of 32 bit Allegro 4 bitmaps. */
#define A4_MASK_COLOR 0x00FF00FF

/* Private state of a BITMAP. */
typedef struct A4_BMP {
    /* the pixels */
    ALLEGRO_BITMAP* bmp;
    /* copy with the mask colour turned transparent, for masked draws */
    ALLEGRO_BITMAP* masked;
    /* TRUE when bmp changed since masked was built */
    int masked_dirty;
    /* non NULL while bmp is locked for direct pixel access */
    ALLEGRO_LOCKED_REGION* lock;
    /* TRUE for the screen bitmap */
    int is_screen;
} A4_BMP;

/* Private state of a FONT. */
struct FONT {
    ALLEGRO_FONT* f;
};

/* Private state of a SAMPLE: the instances it was last played on. */
#define A4_SAMPLE_IDS 16
struct SAMPLE {
    ALLEGRO_SAMPLE* spl;
    ALLEGRO_SAMPLE_ID ids[A4_SAMPLE_IDS];
    int used[A4_SAMPLE_IDS];
    int next;
};

/* Private state of a MIDI: the file to hand to fluidsynth. */
struct MIDI {
    char* path;
};

#define A4P(b) ((A4_BMP*)((b)->priv))

char allegro_error[ALLEGRO_ERROR_SIZE] = "";
BITMAP* screen = NULL;
FONT* font = NULL;
volatile char key[KEY_MAX];
volatile int key_shifts = 0;
volatile int mouse_x = 0;
volatile int mouse_y = 0;
volatile int mouse_z = 0;
volatile int mouse_b = 0;

static int a4_inited = 0;
static ALLEGRO_DISPLAY* a4_display = NULL;
static int a4_gl_mode = 0;
static int a4_screen_dirty = 0;
static int a4_cursor_shown = 0;
static void (*a4_close_cb)(void) = NULL;
/* screen -> display mapping (letterboxed uniform scale) */
static float a4_scale = 1.0f;
static float a4_off_x = 0.0f;
static float a4_off_y = 0.0f;
/* drawing state */
static int a4_draw_mode = DRAW_MODE_SOLID;
static int a4_alpha_blender = 0;
static int a4_trans_alpha = 255;

/* input thread */
static ALLEGRO_EVENT_QUEUE* a4_input_queue = NULL;
static ALLEGRO_THREAD* a4_input_thread = NULL;
static ALLEGRO_MUTEX* a4_input_mutex = NULL;
#define A4_KEYBUF_SIZE 64
static int a4_keybuf[A4_KEYBUF_SIZE];
static int a4_keybuf_start = 0;
static int a4_keybuf_count = 0;
static int a4_mickey_x = 0;
static int a4_mickey_y = 0;
static volatile int a4_close_requested = 0;
/* the last left click, latched by the input thread so that the layer's own
 * menus and boxes never miss a click shorter than a frame */
static int a4_click_pending = 0;
static int a4_click_x = 0;
static int a4_click_y = 0;

/* timer thread */
#define A4_MAX_TIMERS 16
typedef struct A4_TIMER {
    void (*proc)(void);
    double period;
    double next;
} A4_TIMER;
static A4_TIMER a4_timers[A4_MAX_TIMERS];
static ALLEGRO_THREAD* a4_timer_thread = NULL;
static ALLEGRO_MUTEX* a4_timer_mutex = NULL;

/* sound */
static int a4_sound_ok = 0;
static int a4_digi_volume = 255;
static int a4_midi_volume = 255;

static void _a4_midi_shutdown(void);
static void _a4_present(void);
static void _a4_flush(void);

/* File names. */

/* Return a malloc'd copy of path with '\' turned into '/' and, when the
 * name does not exist as written, each component matched case
 * insensitively against the directory content. Windows file systems are
 * case insensitive already, there only the separators are fixed. */
static char* _a4_resolve(const char* path) {
    if (!path) return NULL;
    char* p = strdup(path);
    if (!p) return NULL;
#ifndef __windows__
    for (char* c = p; *c; c++)
        if (*c == '\\') *c = '/';
    if (access(p, F_OK) == 0) return p;

    size_t len = strlen(p);
    char* out = malloc(len + 3);
    if (!out) return p;
    out[0] = '\0';
    char* save = NULL;
    char* work = strdup(p);
    if (!work) {
        free(out);
        return p;
    }
    int absolute = (p[0] == '/');
    if (absolute) strcpy(out, "/");
    for (char* comp = strtok_r(work, "/", &save); comp; comp = strtok_r(NULL, "/", &save)) {
        const char* dir = out[0] ? out : ".";
        char candidate[4096];
        snprintf(candidate, sizeof(candidate), "%s%s%s", out, (out[0] && out[strlen(out) - 1] != '/') ? "/" : "", comp);
        const char* chosen = comp;
        char found[256] = "";
        if (access(candidate, F_OK) != 0) {
            DIR* d = opendir(dir);
            if (d) {
                struct dirent* ent = NULL;
                while ((ent = readdir(d)) != NULL) {
                    if (strcasecmp(ent->d_name, comp) == 0) {
                        snprintf(found, sizeof(found), "%s", ent->d_name);
                        break;
                    }
                }
                closedir(d);
            }
            if (found[0]) chosen = found;
        }
        size_t need = strlen(out) + strlen(chosen) + 2;
        if (need > len + 3) {
            char* bigger = realloc(out, need + 16);
            if (!bigger) break;
            out = bigger;
            len = need + 13;
        }
        if (out[0] && out[strlen(out) - 1] != '/') strcat(out, "/");
        strcat(out, chosen);
    }
    free(work);
    free(p);
    return out;
#else
    for (char* c = p; *c; c++)
        if (*c == '/') *c = '\\';
    return p;
#endif
}

/* strndup() is not available everywhere (MinGW). */
static char* _a4_strndup(const char* s, size_t n) {
    char* r = malloc(n + 1);
    if (!r) return NULL;
    memcpy(r, s, n);
    r[n] = '\0';
    return r;
}

/* Colours. */

uint32_t _a4_pixel_from_color(int color) {
    uint32_t c = (uint32_t)color;
    uint32_t a = c >> 24;
    if (a == 0) a = 255;
    return (a << 24) | (c & 0x00FFFFFFu);
}

static int _a4_color_from_pixel(uint32_t p) {
    if ((p >> 24) == 255) return (int)(p & 0x00FFFFFFu);
    return (int)p;
}

static ALLEGRO_COLOR _a4_rgba(int color, int alpha) {
    uint32_t c = (uint32_t)color;
    return al_map_rgba((unsigned char)((c >> 16) & 0xFF), (unsigned char)((c >> 8) & 0xFF), (unsigned char)(c & 0xFF), (unsigned char)alpha);
}

/* How a primitive is blended onto its target. */
#define A4_BLEND_COPY 0
#define A4_BLEND_ALPHA 1
#define A4_BLEND_FADE 2

/* The colour a primitive is drawn with, and in *blend how to blend it.
 *
 * Allegro 4's 32 bit blenders work in integers: an opacity n mixes with
 * (n + 1) / 256 and the result is truncated, so drawing black at a tiny
 * opacity over the whole buffer every frame (the classic fade / motion
 * blur trick) always takes at least one step off each channel and trails
 * fade out. GPU blending rounds to nearest instead, and 100 * (1 - 1/255)
 * rounds back to 100: dark trails would never go away. So a translucent
 * black is drawn with A4_BLEND_FADE, dest * (1 - n / 256) - 1 / 255,
 * which lands on Allegro 4's dest - ceil(dest * (n + 1) / 256) (100 -> 99,
 * 60 -> 59, 1 -> 0 for n = 1). The bias is a whole step because vertex
 * colours are quantized to 8 bits, where half a step would round to 0. */
static ALLEGRO_COLOR _a4_prim(int color, int* blend) {
    if (a4_draw_mode == DRAW_MODE_TRANS) {
        int n = a4_alpha_blender ? (int)(((uint32_t)color) >> 24) : a4_trans_alpha;
        float a = n > 0 ? (float)(n + 1) / 256.0f : 0.0f;
        if (a > 1.0f) a = 1.0f;
        if ((((uint32_t)color) & 0x00FFFFFFu) == 0 && n > 0 && n < 255) {
            *blend = A4_BLEND_FADE;
            float bias = 1.0f / 255.0f;
            return al_map_rgba_f(bias, bias, bias, (float)n / 256.0f);
        }
        *blend = A4_BLEND_ALPHA;
        uint32_t c = (uint32_t)color;
        return al_map_rgba_f((float)((c >> 16) & 0xFF) / 255.0f, (float)((c >> 8) & 0xFF) / 255.0f, (float)(c & 0xFF) / 255.0f, a);
    }
    *blend = A4_BLEND_COPY;
    return _a4_rgba(color, (int)(_a4_pixel_from_color(color) >> 24));
}

int makecol(int r, int g, int b) {
    return ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF);
}

int makecol32(int r, int g, int b) {
    return makecol(r, g, b);
}

int makeacol(int r, int g, int b, int a) {
    return (int)(((uint32_t)(a & 0xFF) << 24) | (uint32_t)makecol(r, g, b));
}

int makeacol32(int r, int g, int b, int a) {
    return makeacol(r, g, b, a);
}

int getr(int c) {
    return (int)((((uint32_t)c) >> 16) & 0xFF);
}

int getg(int c) {
    return (int)((((uint32_t)c) >> 8) & 0xFF);
}

int getb(int c) {
    return (int)(((uint32_t)c) & 0xFF);
}

int geta(int c) {
    return (int)(_a4_pixel_from_color(c) >> 24);
}

/* Bitmap state helpers. */

static void _a4_unlock(BITMAP* b) {
    if (!b || !b->priv) return;
    A4_BMP* p = A4P(b);
    if (p->lock) {
        al_unlock_bitmap(p->bmp);
        p->lock = NULL;
    }
}

static ALLEGRO_LOCKED_REGION* _a4_lock(BITMAP* b) {
    A4_BMP* p = A4P(b);
    if (!p->lock) {
        _a4_flush();
        p->lock = al_lock_bitmap(p->bmp, ALLEGRO_PIXEL_FORMAT_ARGB_8888, ALLEGRO_LOCK_READWRITE);
    }
    return p->lock;
}

/* Make b the drawing target. The caller fetched its sources first. */
static int _a4_target(BITMAP* b) {
    if (!b || !b->priv) return FALSE;
    A4_BMP* p = A4P(b);
    _a4_flush();
    _a4_unlock(b);
    if (al_get_target_bitmap() != p->bmp) al_set_target_bitmap(p->bmp);
    p->masked_dirty = 1;
    if (p->is_screen) a4_screen_dirty = 1;
    return TRUE;
}

/* After drawing on b: Allegro 4 showed screen drawing immediately. */
static void _a4_done(const BITMAP* b) {
    if (b && b->priv && A4P(b)->is_screen && !a4_gl_mode) _a4_present();
}

/* The Allegro 5 bitmap to draw b from, masked or not. */
static ALLEGRO_BITMAP* _a4_source(BITMAP* b, int masked) {
    if (!b || !b->priv) return NULL;
    A4_BMP* p = A4P(b);
    _a4_flush();
    _a4_unlock(b);
    if (!masked) return p->bmp;
    if (!p->masked || p->masked_dirty) {
        if (!p->masked) {
            p->masked = al_create_bitmap(b->w, b->h);
            if (!p->masked) return p->bmp;
        }
        ALLEGRO_STATE state;
        al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP | ALLEGRO_STATE_BLENDER);
        al_set_target_bitmap(p->masked);
        al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
        al_draw_bitmap(p->bmp, 0, 0, 0);
        al_convert_mask_to_alpha(p->masked, al_map_rgb(255, 0, 255));
        al_restore_state(&state);
        p->masked_dirty = 0;
    }
    return p->masked;
}

static BITMAP* _a4_wrap(ALLEGRO_BITMAP* abmp) {
    if (!abmp) return NULL;
    BITMAP* b = NULL;
    A4_BMP* p = NULL;
    Malloc(b, BITMAP, 1);
    Malloc(p, A4_BMP, 1);
    if (!b || !p) {
        Free(b);
        Free(p);
        al_destroy_bitmap(abmp);
        return NULL;
    }
    p->bmp = abmp;
    p->masked_dirty = 1;
    b->w = al_get_bitmap_width(abmp);
    b->h = al_get_bitmap_height(abmp);
    b->line = NULL;
    b->priv = p;
    return b;
}

/* Blender for masked and alpha draws of non premultiplied pixels. */
static void _a4_blend_alpha(void) {
    al_set_separate_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA, ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
}

/* Blender for plain copies. */
static void _a4_blend_copy(void) {
    al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
}

static void _a4_set_blend(int blend) {
    if (blend == A4_BLEND_FADE)
        /* dest * (1 - src alpha) - src, alpha channel left alone */
        al_set_separate_blender(ALLEGRO_DEST_MINUS_SRC, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA, ALLEGRO_ADD, ALLEGRO_ZERO, ALLEGRO_ONE);
    else if (blend == A4_BLEND_ALPHA)
        _a4_blend_alpha();
    else
        _a4_blend_copy();
}

/* The colour of a primitive drawn right away, blender set accordingly. */
static ALLEGRO_COLOR _a4_prim_color(int color) {
    int blend = A4_BLEND_COPY;
    ALLEGRO_COLOR c = _a4_prim(color, &blend);
    _a4_set_blend(blend);
    return c;
}

/* Primitive batching. Allegro 4 programs draw thousands of lines and
 * rectangles per frame, one call each, which Allegro 4 rasterized in
 * software. Issuing one GPU draw per call is far too slow, so line(),
 * rectfill(), rect(), triangle() and translucent putpixel() append
 * triangles here, drawn in one al_draw_prim() call. The batch is flushed
 * before anything else is drawn, before a bitmap is used as a source or
 * locked, and before the screen is shown, so drawing order is unchanged. */
#define A4_BATCH_MAX 6144
static ALLEGRO_VERTEX a4_batch[A4_BATCH_MAX];
static int a4_batch_n = 0;
static BITMAP* a4_batch_target = NULL;
static int a4_batch_blend = A4_BLEND_COPY;

static void _a4_flush(void) {
    if (a4_batch_n > 0 && a4_batch_target && a4_batch_target->priv) {
        ALLEGRO_BITMAP* t = A4P(a4_batch_target)->bmp;
        if (al_get_target_bitmap() != t) al_set_target_bitmap(t);
        _a4_set_blend(a4_batch_blend);
        al_draw_prim(a4_batch, NULL, NULL, 0, a4_batch_n, ALLEGRO_PRIM_TRIANGLE_LIST);
    }
    a4_batch_n = 0;
    a4_batch_target = NULL;
}

/* Room for n vertices drawn on b with the given blend mode. */
static ALLEGRO_VERTEX* _a4_reserve(BITMAP* b, int blend, int n) {
    if (a4_batch_target != b || a4_batch_blend != blend || a4_batch_n + n > A4_BATCH_MAX) {
        _a4_flush();
        _a4_unlock(b);
        a4_batch_target = b;
        a4_batch_blend = blend;
    }
    A4_BMP* p = A4P(b);
    p->masked_dirty = 1;
    if (p->is_screen) a4_screen_dirty = 1;
    ALLEGRO_VERTEX* v = &a4_batch[a4_batch_n];
    a4_batch_n += n;
    return v;
}

static void _a4_vertex(ALLEGRO_VERTEX* v, float x, float y, ALLEGRO_COLOR c) {
    v->x = x;
    v->y = y;
    v->z = 0.0f;
    v->u = 0.0f;
    v->v = 0.0f;
    v->color = c;
}

/* A quad given by its four corners in order. */
static void _a4_quad(BITMAP* b, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, int color) {
    int blend = A4_BLEND_COPY;
    ALLEGRO_COLOR c = _a4_prim(color, &blend);
    ALLEGRO_VERTEX* v = _a4_reserve(b, blend, 6);
    _a4_vertex(&v[0], x1, y1, c);
    _a4_vertex(&v[1], x2, y2, c);
    _a4_vertex(&v[2], x3, y3, c);
    _a4_vertex(&v[3], x1, y1, c);
    _a4_vertex(&v[4], x3, y3, c);
    _a4_vertex(&v[5], x4, y4, c);
}

/* An axis aligned box covering pixels [l, r) x [t, bt). */
static void _a4_box(BITMAP* b, float l, float t, float r, float bt, int color) {
    _a4_quad(b, l, t, r, t, r, bt, l, bt, color);
}

/* System. */

static void _a4_input_handle(const ALLEGRO_EVENT* ev) {
    switch (ev->type) {
        case ALLEGRO_EVENT_KEY_DOWN:
            if (ev->keyboard.keycode > 0 && ev->keyboard.keycode < KEY_MAX) key[ev->keyboard.keycode] = 1;
            break;
        case ALLEGRO_EVENT_KEY_UP:
            if (ev->keyboard.keycode > 0 && ev->keyboard.keycode < KEY_MAX) key[ev->keyboard.keycode] = 0;
            break;
        case ALLEGRO_EVENT_KEY_CHAR: {
            int ascii = (ev->keyboard.unichar > 0 && ev->keyboard.unichar < 256) ? ev->keyboard.unichar : 0;
            int k = (ev->keyboard.keycode << 8) | ascii;
            al_lock_mutex(a4_input_mutex);
            if (a4_keybuf_count < A4_KEYBUF_SIZE) {
                a4_keybuf[(a4_keybuf_start + a4_keybuf_count) % A4_KEYBUF_SIZE] = k;
                a4_keybuf_count++;
            }
            al_unlock_mutex(a4_input_mutex);
            break;
        }
        case ALLEGRO_EVENT_MOUSE_AXES:
        case ALLEGRO_EVENT_MOUSE_WARPED:
            mouse_x = (int)(((float)ev->mouse.x - a4_off_x) / a4_scale);
            mouse_y = (int)(((float)ev->mouse.y - a4_off_y) / a4_scale);
            mouse_z = ev->mouse.z;
            al_lock_mutex(a4_input_mutex);
            a4_mickey_x += (int)((float)ev->mouse.dx / a4_scale);
            a4_mickey_y += (int)((float)ev->mouse.dy / a4_scale);
            al_unlock_mutex(a4_input_mutex);
            break;
        case ALLEGRO_EVENT_MOUSE_BUTTON_DOWN:
            if (ev->mouse.button >= 1 && ev->mouse.button <= 3) mouse_b |= (1 << (ev->mouse.button - 1));
            if (ev->mouse.button == 1) {
                al_lock_mutex(a4_input_mutex);
                a4_click_pending = 1;
                a4_click_x = (int)(((float)ev->mouse.x - a4_off_x) / a4_scale);
                a4_click_y = (int)(((float)ev->mouse.y - a4_off_y) / a4_scale);
                al_unlock_mutex(a4_input_mutex);
            }
            break;
        case ALLEGRO_EVENT_MOUSE_BUTTON_UP:
            if (ev->mouse.button >= 1 && ev->mouse.button <= 3) mouse_b &= ~(1 << (ev->mouse.button - 1));
            break;
        case ALLEGRO_EVENT_DISPLAY_SWITCH_OUT:
            memset((void*)key, 0, sizeof(key));
            mouse_b = 0;
            break;
        case ALLEGRO_EVENT_DISPLAY_CLOSE:
            a4_close_requested = 1;
            if (a4_close_cb) a4_close_cb();
            break;
        default:
            break;
    }
}

static void* _a4_input_proc(ALLEGRO_THREAD* thr, void* arg) {
    (void)arg;
    while (!al_get_thread_should_stop(thr)) {
        ALLEGRO_EVENT ev;
        if (al_wait_for_event_timed(a4_input_queue, &ev, 0.05f)) _a4_input_handle(&ev);
    }
    return NULL;
}

static void* _a4_timer_proc(ALLEGRO_THREAD* thr, void* arg) {
    (void)arg;
    while (!al_get_thread_should_stop(thr)) {
        double now = al_get_time();
        double wait = 0.01;
        void (*due[A4_MAX_TIMERS])(void);
        int nb_due = 0;
        al_lock_mutex(a4_timer_mutex);
        for (int i = 0; i < A4_MAX_TIMERS; i++) {
            if (!a4_timers[i].proc) continue;
            if (now >= a4_timers[i].next) {
                due[nb_due++] = a4_timers[i].proc;
                a4_timers[i].next += a4_timers[i].period;
                if (a4_timers[i].next < now) a4_timers[i].next = now + a4_timers[i].period;
            }
            double left = a4_timers[i].next - now;
            if (left < wait) wait = left;
        }
        al_unlock_mutex(a4_timer_mutex);
        for (int i = 0; i < nb_due; i++) due[i]();
        if (wait > 0.0) al_rest(wait);
    }
    return NULL;
}

int allegro_init(void) {
    if (a4_inited) return 0;
    if (!al_init()) {
        snprintf(allegro_error, sizeof(allegro_error), "al_init failed");
        return -1;
    }
    al_init_primitives_addon();
    al_init_image_addon();
    al_init_font_addon();
    al_init_ttf_addon();
    memset((void*)key, 0, sizeof(key));
    memset(a4_timers, 0, sizeof(a4_timers));
    a4_input_mutex = al_create_mutex();
    a4_timer_mutex = al_create_mutex();
    a4_input_queue = al_create_event_queue();
    if (!a4_input_mutex || !a4_timer_mutex || !a4_input_queue) {
        snprintf(allegro_error, sizeof(allegro_error), "could not create the input queue");
        return -1;
    }
    a4_input_thread = al_create_thread(_a4_input_proc, NULL);
    if (a4_input_thread) al_start_thread(a4_input_thread);
    a4_timer_thread = al_create_thread(_a4_timer_proc, NULL);
    if (a4_timer_thread) al_start_thread(a4_timer_thread);

    FONT* f = NULL;
    Malloc(f, FONT, 1);
    if (f) {
        f->f = al_create_builtin_font();
        font = f;
    }
    a4_inited = 1;
    atexit(allegro_exit);
    return 0;
}

static void _a4_destroy_display(void) {
    if (screen) {
        destroy_bitmap(screen);
        screen = NULL;
    }
    if (a4_display) {
        if (a4_input_queue) al_unregister_event_source(a4_input_queue, al_get_display_event_source(a4_display));
        al_destroy_display(a4_display);
        a4_display = NULL;
    }
}

void allegro_exit(void) {
    if (!a4_inited) return;
    a4_inited = 0;
    if (a4_timer_thread) {
        al_destroy_thread(a4_timer_thread);
        a4_timer_thread = NULL;
    }
    if (a4_input_thread) {
        al_destroy_thread(a4_input_thread);
        a4_input_thread = NULL;
    }
    remove_sound();
    _a4_destroy_display();
    if (font) {
        if (font->f) al_destroy_font(font->f);
        Free(font);
        font = NULL;
    }
    if (a4_input_queue) {
        al_destroy_event_queue(a4_input_queue);
        a4_input_queue = NULL;
    }
    if (a4_input_mutex) {
        al_destroy_mutex(a4_input_mutex);
        a4_input_mutex = NULL;
    }
    if (a4_timer_mutex) {
        al_destroy_mutex(a4_timer_mutex);
        a4_timer_mutex = NULL;
    }
}

void set_uformat(int type) {
    (void)type;
}

int ustricmp(const char* s1, const char* s2) {
    return strcasecmp(s1, s2);
}

void set_color_depth(int depth) {
    if (depth != 32) n_log(LOG_DEBUG, "color depth %d requested, using 32", depth);
}

int get_color_depth(void) {
    return 32;
}

static void _a4_compute_scale(void) {
    if (!a4_display || !screen) return;
    float dw = (float)al_get_display_width(a4_display);
    float dh = (float)al_get_display_height(a4_display);
    float sx = dw / (float)screen->w;
    float sy = dh / (float)screen->h;
    a4_scale = sx < sy ? sx : sy;
    if (a4_scale <= 0.0f) a4_scale = 1.0f;
    a4_off_x = (dw - (float)screen->w * a4_scale) / 2.0f;
    a4_off_y = (dh - (float)screen->h * a4_scale) / 2.0f;
}

int set_gfx_mode(int card, int w, int h, int v_w, int v_h) {
    (void)v_w;
    (void)v_h;
    if (!a4_inited && allegro_init() != 0) return -1;
    _a4_destroy_display();
    if (card == GFX_TEXT) return 0;

    int flags = ALLEGRO_WINDOWED;
    if (card == GFX_AUTODETECT_FULLSCREEN || card == GFX_DIRECTX || card == GFX_OPENGL_FULLSCREEN) flags = ALLEGRO_FULLSCREEN_WINDOW;
    al_set_new_display_flags(flags);
    a4_display = al_create_display(w, h);
    if (!a4_display) {
        snprintf(allegro_error, sizeof(allegro_error), "could not create a %dx%d display", w, h);
        return -1;
    }
    a4_gl_mode = (card == GFX_OPENGL_WINDOWED || card == GFX_OPENGL_FULLSCREEN || card == GFX_OPENGL);
    al_register_event_source(a4_input_queue, al_get_display_event_source(a4_display));
    if (a4_cursor_shown)
        al_show_mouse_cursor(a4_display);
    else
        al_hide_mouse_cursor(a4_display);

    screen = create_bitmap(w, h);
    if (!screen) {
        snprintf(allegro_error, sizeof(allegro_error), "could not create the screen bitmap");
        _a4_destroy_display();
        return -1;
    }
    A4P(screen)->is_screen = 1;
    _a4_compute_scale();
    _a4_present();
    return 0;
}

void set_window_title(const char* name) {
    if (a4_display && name) al_set_window_title(a4_display, name);
}

int set_close_button_callback(void (*proc)(void)) {
    a4_close_cb = proc;
    return 0;
}

/* Draw screen on the display, letterboxed, and flip. */
static void _a4_present(void) {
    if (!a4_display || !screen) return;
    _a4_flush();
    ALLEGRO_BITMAP* src = _a4_source(screen, FALSE);
    al_set_target_backbuffer(a4_display);
    al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
    if (a4_off_x > 0.0f || a4_off_y > 0.0f) al_clear_to_color(al_map_rgb(0, 0, 0));
    al_draw_scaled_bitmap(src, 0, 0, (float)screen->w, (float)screen->h, a4_off_x, a4_off_y, (float)screen->w * a4_scale, (float)screen->h * a4_scale, 0);
    al_flip_display();
    a4_screen_dirty = 0;
}

int install_allegro_gl(void) {
    return 0;
}

void allegro_gl_set_allegro_mode(void) {
}

void allegro_gl_unset_allegro_mode(void) {
}

void allegro_gl_flip(void) {
    if (a4_gl_mode || a4_screen_dirty) _a4_present();
}

/* Keyboard and mouse. */

int install_keyboard(void) {
    if (!a4_inited && allegro_init() != 0) return -1;
    if (!al_is_keyboard_installed() && !al_install_keyboard()) return -1;
    al_register_event_source(a4_input_queue, al_get_keyboard_event_source());
    return 0;
}

int poll_keyboard(void) {
    return 0;
}

/* Take the latched left click, TRUE and its position if there was one. */
static int _a4_take_click(int* x, int* y) {
    if (!a4_input_mutex) return FALSE;
    al_lock_mutex(a4_input_mutex);
    int r = a4_click_pending;
    if (r) {
        *x = a4_click_x;
        *y = a4_click_y;
        a4_click_pending = 0;
    }
    al_unlock_mutex(a4_input_mutex);
    return r;
}

int keypressed(void) {
    if (!a4_input_mutex) return FALSE;
    al_lock_mutex(a4_input_mutex);
    int r = a4_keybuf_count > 0;
    al_unlock_mutex(a4_input_mutex);
    return r;
}

int readkey(void) {
    if (!a4_input_mutex) return 0;
    for (;;) {
        al_lock_mutex(a4_input_mutex);
        if (a4_keybuf_count > 0) {
            int k = a4_keybuf[a4_keybuf_start];
            a4_keybuf_start = (a4_keybuf_start + 1) % A4_KEYBUF_SIZE;
            a4_keybuf_count--;
            al_unlock_mutex(a4_input_mutex);
            return k;
        }
        al_unlock_mutex(a4_input_mutex);
        al_rest(0.005);
    }
}

void clear_keybuf(void) {
    if (!a4_input_mutex) return;
    al_lock_mutex(a4_input_mutex);
    a4_keybuf_start = 0;
    a4_keybuf_count = 0;
    al_unlock_mutex(a4_input_mutex);
}

int install_mouse(void) {
    if (!a4_inited && allegro_init() != 0) return -1;
    if (!al_is_mouse_installed() && !al_install_mouse()) return -1;
    al_register_event_source(a4_input_queue, al_get_mouse_event_source());
    return 3;
}

int poll_mouse(void) {
    return 0;
}

// The Allegro 4 signature takes a non const BITMAP*, kept for source compatibility.
// cppcheck-suppress constParameterPointer
// cppcheck-suppress constParameter
void show_mouse(BITMAP* bmp) {
    a4_cursor_shown = (bmp != NULL);
    if (!a4_display) return;
    if (a4_cursor_shown)
        al_show_mouse_cursor(a4_display);
    else
        al_hide_mouse_cursor(a4_display);
}

void get_mouse_mickeys(int* mickeyx, int* mickeyy) {
    if (!a4_input_mutex) return;
    al_lock_mutex(a4_input_mutex);
    if (mickeyx) *mickeyx = a4_mickey_x;
    if (mickeyy) *mickeyy = a4_mickey_y;
    a4_mickey_x = 0;
    a4_mickey_y = 0;
    al_unlock_mutex(a4_input_mutex);
}

void position_mouse(int x, int y) {
    if (!a4_display) return;
    al_set_mouse_xy(a4_display, (int)((float)x * a4_scale + a4_off_x), (int)((float)y * a4_scale + a4_off_y));
    mouse_x = x;
    mouse_y = y;
}

/* Timers. */

int install_timer(void) {
    return a4_inited ? 0 : allegro_init();
}

int install_int_ex(void (*proc)(void), long speed) {
    if (!proc || speed <= 0) return -1;
    if (!a4_inited && allegro_init() != 0) return -1;
    double period = (double)speed / (double)TIMERS_PER_SECOND;
    int ret = -1;
    al_lock_mutex(a4_timer_mutex);
    for (int i = 0; i < A4_MAX_TIMERS; i++) {
        if (a4_timers[i].proc == proc) {
            a4_timers[i].period = period;
            ret = 0;
            break;
        }
    }
    for (int i = 0; ret != 0 && i < A4_MAX_TIMERS; i++) {
        if (!a4_timers[i].proc) {
            a4_timers[i].proc = proc;
            a4_timers[i].period = period;
            a4_timers[i].next = al_get_time() + period;
            ret = 0;
        }
    }
    al_unlock_mutex(a4_timer_mutex);
    return ret;
}

int install_int(void (*proc)(void), int speed) {
    return install_int_ex(proc, MSEC_TO_TIMER(speed));
}

// cppcheck-suppress constParameter
void remove_int(void (*proc)(void)) {
    if (!a4_timer_mutex) return;
    al_lock_mutex(a4_timer_mutex);
    for (int i = 0; i < A4_MAX_TIMERS; i++)
        if (a4_timers[i].proc == proc) a4_timers[i].proc = NULL;
    al_unlock_mutex(a4_timer_mutex);
}

void rest(unsigned int ms) {
    al_rest((double)ms / 1000.0);
}

/* Bitmaps. */

BITMAP* create_bitmap(int width, int height) {
    if (width <= 0 || height <= 0) return NULL;
    al_set_new_bitmap_format(ALLEGRO_PIXEL_FORMAT_ANY_32_WITH_ALPHA);
    ALLEGRO_BITMAP* abmp = al_create_bitmap(width, height);
    if (!abmp) {
        snprintf(allegro_error, sizeof(allegro_error), "could not create a %dx%d bitmap", width, height);
        return NULL;
    }
    ALLEGRO_STATE state;
    al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP);
    al_set_target_bitmap(abmp);
    al_clear_to_color(al_map_rgb(0, 0, 0));
    al_restore_state(&state);
    return _a4_wrap(abmp);
}

BITMAP* create_video_bitmap(int width, int height) {
    return create_bitmap(width, height);
}

BITMAP* create_bitmap_ex(int depth, int width, int height) {
    (void)depth;
    return create_bitmap(width, height);
}

void destroy_bitmap(BITMAP* bmp) {
    if (!bmp) return;
    A4_BMP* p = A4P(bmp);
    if (a4_batch_target == bmp) {
        a4_batch_n = 0;
        a4_batch_target = NULL;
    }
    if (p) {
        _a4_unlock(bmp);
        if (p->masked) al_destroy_bitmap(p->masked);
        if (p->bmp) al_destroy_bitmap(p->bmp);
        Free(p);
    }
    if (bmp == screen) screen = NULL;
    Free(bmp);
}

BITMAP* load_bitmap(const char* filename, RGB* pal) {
    (void)pal;
    char* path = _a4_resolve(filename);
    if (!path) return NULL;
    al_set_new_bitmap_format(ALLEGRO_PIXEL_FORMAT_ANY_32_WITH_ALPHA);
    ALLEGRO_BITMAP* abmp = al_load_bitmap_flags(path, ALLEGRO_NO_PREMULTIPLIED_ALPHA);
    if (!abmp) {
        snprintf(allegro_error, sizeof(allegro_error), "could not load %s", path);
        n_log(LOG_ERR, "could not load bitmap %s", path);
    }
    free(path);
    return _a4_wrap(abmp);
}

int save_bitmap(const char* filename, BITMAP* bmp, const RGB* pal) {
    (void)pal;
    if (!bmp || !filename) return -1;
    _a4_unlock(bmp);
    return al_save_bitmap(filename, A4P(bmp)->bmp) ? 0 : -1;
}

int bitmap_color_depth(BITMAP* bmp) {
    (void)bmp;
    return 32;
}

int bitmap_mask_color(BITMAP* bmp) {
    (void)bmp;
    return A4_MASK_COLOR;
}

int is_memory_bitmap(BITMAP* bmp) {
    (void)bmp;
    return FALSE;
}

void clear_to_color(BITMAP* bmp, int color) {
    if (!_a4_target(bmp)) return;
    al_clear_to_color(_a4_rgba(color, (int)(_a4_pixel_from_color(color) >> 24)));
    _a4_done(bmp);
}

void clear(BITMAP* bmp) {
    clear_to_color(bmp, 0);
}

void clear_bitmap(BITMAP* bmp) {
    clear_to_color(bmp, 0);
}

/* Draw a region of source onto dest, through a temporary copy when they
 * are the same bitmap (Allegro 4 allowed overlapping blits). */
static void _a4_draw_region(BITMAP* source, BITMAP* dest, int masked, float sx, float sy, float sw, float sh, float dx, float dy, float dw, float dh, int flags) {
    if (!source || !dest || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0) return;
    ALLEGRO_BITMAP* src = _a4_source(source, masked);
    ALLEGRO_BITMAP* tmp = NULL;
    if (source == dest) {
        tmp = al_create_bitmap((int)sw, (int)sh);
        if (!tmp) return;
        al_set_target_bitmap(tmp);
        al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
        al_draw_bitmap_region(src, sx, sy, sw, sh, 0, 0, 0);
        src = tmp;
        sx = 0;
        sy = 0;
    }
    if (_a4_target(dest)) {
        if (masked)
            _a4_blend_alpha();
        else
            _a4_blend_copy();
        al_draw_scaled_bitmap(src, sx, sy, sw, sh, dx, dy, dw, dh, flags);
    }
    if (tmp) al_destroy_bitmap(tmp);
    _a4_done(dest);
}

void blit(BITMAP* source, BITMAP* dest, int source_x, int source_y, int dest_x, int dest_y, int width, int height) {
    _a4_draw_region(source, dest, FALSE, (float)source_x, (float)source_y, (float)width, (float)height, (float)dest_x, (float)dest_y, (float)width, (float)height, 0);
}

void masked_blit(BITMAP* source, BITMAP* dest, int source_x, int source_y, int dest_x, int dest_y, int width, int height) {
    _a4_draw_region(source, dest, TRUE, (float)source_x, (float)source_y, (float)width, (float)height, (float)dest_x, (float)dest_y, (float)width, (float)height, 0);
}

void stretch_blit(BITMAP* source, BITMAP* dest, int source_x, int source_y, int source_width, int source_height, int dest_x, int dest_y, int dest_width, int dest_height) {
    _a4_draw_region(source, dest, FALSE, (float)source_x, (float)source_y, (float)source_width, (float)source_height, (float)dest_x, (float)dest_y, (float)dest_width, (float)dest_height, 0);
}

void masked_stretch_blit(BITMAP* source, BITMAP* dest, int source_x, int source_y, int source_w, int source_h, int dest_x, int dest_y, int dest_w, int dest_h) {
    _a4_draw_region(source, dest, TRUE, (float)source_x, (float)source_y, (float)source_w, (float)source_h, (float)dest_x, (float)dest_y, (float)dest_w, (float)dest_h, 0);
}

static void _a4_sprite(BITMAP* bmp, BITMAP* sprite, int x, int y, int flags) {
    if (!sprite) return;
    _a4_draw_region(sprite, bmp, TRUE, 0, 0, (float)sprite->w, (float)sprite->h, (float)x, (float)y, (float)sprite->w, (float)sprite->h, flags);
}

void draw_sprite(BITMAP* bmp, BITMAP* sprite, int x, int y) {
    _a4_sprite(bmp, sprite, x, y, 0);
}

void draw_sprite_h_flip(BITMAP* bmp, BITMAP* sprite, int x, int y) {
    _a4_sprite(bmp, sprite, x, y, ALLEGRO_FLIP_HORIZONTAL);
}

void draw_sprite_v_flip(BITMAP* bmp, BITMAP* sprite, int x, int y) {
    _a4_sprite(bmp, sprite, x, y, ALLEGRO_FLIP_VERTICAL);
}

void draw_sprite_vh_flip(BITMAP* bmp, BITMAP* sprite, int x, int y) {
    _a4_sprite(bmp, sprite, x, y, ALLEGRO_FLIP_HORIZONTAL | ALLEGRO_FLIP_VERTICAL);
}

void stretch_sprite(BITMAP* bmp, BITMAP* sprite, int x, int y, int w, int h) {
    if (!sprite) return;
    _a4_draw_region(sprite, bmp, TRUE, 0, 0, (float)sprite->w, (float)sprite->h, (float)x, (float)y, (float)w, (float)h, 0);
}

void draw_trans_sprite(BITMAP* bmp, BITMAP* sprite, int x, int y) {
    if (!sprite || !bmp || sprite == bmp) return;
    /* the alpha blender uses the sprite's own alpha channel, the trans
     * blender a constant opacity over the masked sprite */
    ALLEGRO_BITMAP* src = _a4_source(sprite, !a4_alpha_blender);
    if (!_a4_target(bmp)) return;
    _a4_blend_alpha();
    if (a4_alpha_blender)
        al_draw_bitmap(src, (float)x, (float)y, 0);
    else
        al_draw_tinted_bitmap(src, al_map_rgba_f(1, 1, 1, (float)a4_trans_alpha / 255.0f), (float)x, (float)y, 0);
    _a4_done(bmp);
}

static void _a4_rotated(BITMAP* bmp, BITMAP* sprite, float x, float y, float cx, float cy, fixed angle, fixed scale) {
    if (!sprite || !bmp || sprite == bmp) return;
    ALLEGRO_BITMAP* src = _a4_source(sprite, TRUE);
    if (!_a4_target(bmp)) return;
    _a4_blend_alpha();
    float radians = (float)(fixtof(angle) * ALLEGRO_PI / 128.0);
    float s = (float)fixtof(scale);
    al_draw_scaled_rotated_bitmap(src, cx, cy, x, y, s, s, radians, 0);
    _a4_done(bmp);
}

void rotate_sprite(BITMAP* bmp, BITMAP* sprite, int x, int y, fixed angle) {
    if (!sprite) return;
    _a4_rotated(bmp, sprite, (float)x + (float)sprite->w / 2.0f, (float)y + (float)sprite->h / 2.0f, (float)sprite->w / 2.0f, (float)sprite->h / 2.0f, angle, itofix(1));
}

void rotate_scaled_sprite(BITMAP* bmp, BITMAP* sprite, int x, int y, fixed angle, fixed scale) {
    if (!sprite) return;
    float s = (float)fixtof(scale);
    _a4_rotated(bmp, sprite, (float)x + (float)sprite->w * s / 2.0f, (float)y + (float)sprite->h * s / 2.0f, (float)sprite->w / 2.0f, (float)sprite->h / 2.0f, angle, scale);
}

void pivot_sprite(BITMAP* bmp, BITMAP* sprite, int x, int y, int cx, int cy, fixed angle) {
    _a4_rotated(bmp, sprite, (float)x, (float)y, (float)cx, (float)cy, angle, itofix(1));
}

void pivot_scaled_sprite(BITMAP* bmp, BITMAP* sprite, int x, int y, int cx, int cy, fixed angle, fixed scale) {
    _a4_rotated(bmp, sprite, (float)x, (float)y, (float)cx, (float)cy, angle, scale);
}

/* Primitives. Allegro 4 coordinates are inclusive pixel indexes. */

void drawing_mode(int mode, BITMAP* pattern, int x_anchor, int y_anchor) {
    (void)pattern;
    (void)x_anchor;
    (void)y_anchor;
    a4_draw_mode = (mode == DRAW_MODE_TRANS) ? DRAW_MODE_TRANS : DRAW_MODE_SOLID;
}

void solid_mode(void) {
    a4_draw_mode = DRAW_MODE_SOLID;
}

void set_trans_blender(int r, int g, int b, int a) {
    (void)r;
    (void)g;
    (void)b;
    a4_alpha_blender = 0;
    a4_trans_alpha = a < 0 ? 0 : (a > 255 ? 255 : a);
}

void set_alpha_blender(void) {
    a4_alpha_blender = 1;
}

void putpixel(BITMAP* bmp, int x, int y, int color) {
    if (!bmp || x < 0 || y < 0 || x >= bmp->w || y >= bmp->h) return;
    if (a4_draw_mode == DRAW_MODE_TRANS) {
        if (!bmp->priv) return;
        _a4_box(bmp, (float)x, (float)y, (float)x + 1.0f, (float)y + 1.0f, color);
        _a4_done(bmp);
        return;
    }
    ALLEGRO_LOCKED_REGION* lr = _a4_lock(bmp);
    if (!lr) return;
    uint32_t* row = (uint32_t*)((char*)lr->data + (ptrdiff_t)y * lr->pitch);
    row[x] = _a4_pixel_from_color(color);
    A4P(bmp)->masked_dirty = 1;
    if (A4P(bmp)->is_screen) a4_screen_dirty = 1;
}

int getpixel(BITMAP* bmp, int x, int y) {
    if (!bmp || x < 0 || y < 0 || x >= bmp->w || y >= bmp->h) return -1;
    const ALLEGRO_LOCKED_REGION* lr = _a4_lock(bmp);
    if (!lr) return -1;
    const uint32_t* row = (const uint32_t*)((const char*)lr->data + (ptrdiff_t)y * lr->pitch);
    return _a4_color_from_pixel(row[x]);
}

void line(BITMAP* bmp, int x1, int y1, int x2, int y2, int color) {
    if (!bmp || !bmp->priv) return;
    /* a one pixel wide quad through the pixel centres, reaching half a
     * pixel past both ends: both end points are drawn, as in Allegro 4 */
    float ax = (float)x1 + 0.5f, ay = (float)y1 + 0.5f;
    float bx = (float)x2 + 0.5f, by = (float)y2 + 0.5f;
    float dx = bx - ax, dy = by - ay;
    float len = sqrtf(dx * dx + dy * dy);
    if (len < 0.5f) {
        _a4_box(bmp, (float)x1, (float)y1, (float)x1 + 1.0f, (float)y1 + 1.0f, color);
    } else {
        float ux = dx / len * 0.5f, uy = dy / len * 0.5f;
        ax -= ux;
        ay -= uy;
        bx += ux;
        by += uy;
        _a4_quad(bmp, ax - uy, ay + ux, bx - uy, by + ux, bx + uy, by - ux, ax + uy, ay - ux, color);
    }
    _a4_done(bmp);
}

void hline(BITMAP* bmp, int x1, int y, int x2, int color) {
    rectfill(bmp, x1, y, x2, y, color);
}

void vline(BITMAP* bmp, int x, int y1, int y2, int color) {
    rectfill(bmp, x, y1, x, y2, color);
}

void rect(BITMAP* bmp, int x1, int y1, int x2, int y2, int color) {
    if (!bmp || !bmp->priv) return;
    if (x1 > x2) {
        int t = x1;
        x1 = x2;
        x2 = t;
    }
    if (y1 > y2) {
        int t = y1;
        y1 = y2;
        y2 = t;
    }
    _a4_box(bmp, (float)x1, (float)y1, (float)x2 + 1.0f, (float)y1 + 1.0f, color);
    if (y2 > y1) _a4_box(bmp, (float)x1, (float)y2, (float)x2 + 1.0f, (float)y2 + 1.0f, color);
    if (y2 - y1 > 1) {
        _a4_box(bmp, (float)x1, (float)y1 + 1.0f, (float)x1 + 1.0f, (float)y2, color);
        if (x2 > x1) _a4_box(bmp, (float)x2, (float)y1 + 1.0f, (float)x2 + 1.0f, (float)y2, color);
    }
    _a4_done(bmp);
}

void rectfill(BITMAP* bmp, int x1, int y1, int x2, int y2, int color) {
    if (!bmp || !bmp->priv) return;
    if (x1 > x2) {
        int t = x1;
        x1 = x2;
        x2 = t;
    }
    if (y1 > y2) {
        int t = y1;
        y1 = y2;
        y2 = t;
    }
    _a4_box(bmp, (float)x1, (float)y1, (float)x2 + 1.0f, (float)y2 + 1.0f, color);
    _a4_done(bmp);
}

void circle(BITMAP* bmp, int x, int y, int radius, int color) {
    if (!_a4_target(bmp)) return;
    al_draw_circle((float)x + 0.5f, (float)y + 0.5f, (float)radius, _a4_prim_color(color), 1.0f);
    _a4_done(bmp);
}

void circlefill(BITMAP* bmp, int x, int y, int radius, int color) {
    if (!_a4_target(bmp)) return;
    al_draw_filled_circle((float)x + 0.5f, (float)y + 0.5f, (float)radius + 0.5f, _a4_prim_color(color));
    _a4_done(bmp);
}

void ellipse(BITMAP* bmp, int x, int y, int rx, int ry, int color) {
    if (!_a4_target(bmp)) return;
    al_draw_ellipse((float)x + 0.5f, (float)y + 0.5f, (float)rx, (float)ry, _a4_prim_color(color), 1.0f);
    _a4_done(bmp);
}

void ellipsefill(BITMAP* bmp, int x, int y, int rx, int ry, int color) {
    if (!_a4_target(bmp)) return;
    al_draw_filled_ellipse((float)x + 0.5f, (float)y + 0.5f, (float)rx + 0.5f, (float)ry + 0.5f, _a4_prim_color(color));
    _a4_done(bmp);
}

void triangle(BITMAP* bmp, int x1, int y1, int x2, int y2, int x3, int y3, int color) {
    if (!bmp || !bmp->priv) return;
    int blend = A4_BLEND_COPY;
    ALLEGRO_COLOR c = _a4_prim(color, &blend);
    ALLEGRO_VERTEX* v = _a4_reserve(bmp, blend, 3);
    _a4_vertex(&v[0], (float)x1 + 0.5f, (float)y1 + 0.5f, c);
    _a4_vertex(&v[1], (float)x2 + 0.5f, (float)y2 + 0.5f, c);
    _a4_vertex(&v[2], (float)x3 + 0.5f, (float)y3 + 0.5f, c);
    _a4_done(bmp);
}

/* Direct pixel access. */

void bmp_select(BITMAP* bmp) {
    (void)bmp;
}

uintptr_t bmp_write_line(BITMAP* bmp, int y) {
    if (!bmp || y < 0 || y >= bmp->h) return 0;
    ALLEGRO_LOCKED_REGION* lr = _a4_lock(bmp);
    if (!lr) return 0;
    A4P(bmp)->masked_dirty = 1;
    if (A4P(bmp)->is_screen) a4_screen_dirty = 1;
    return (uintptr_t)((char*)lr->data + (ptrdiff_t)y * lr->pitch);
}

uintptr_t bmp_read_line(BITMAP* bmp, int y) {
    if (!bmp || y < 0 || y >= bmp->h) return 0;
    ALLEGRO_LOCKED_REGION* lr = _a4_lock(bmp);
    if (!lr) return 0;
    return (uintptr_t)((char*)lr->data + (ptrdiff_t)y * lr->pitch);
}

void bmp_unwrite_line(BITMAP* bmp) {
    (void)bmp;
}

/* Text. */

static ALLEGRO_FONT* _a4_font(const FONT* f) {
    if (f && f->f) return f->f;
    return (font && font->f) ? font->f : NULL;
}

int text_length(const FONT* f, const char* str) {
    ALLEGRO_FONT* af = _a4_font(f);
    return (af && str) ? al_get_text_width(af, str) : 0;
}

int text_height(const FONT* f) {
    ALLEGRO_FONT* af = _a4_font(f);
    return af ? al_get_font_line_height(af) : 0;
}

/* align: 0 left, 1 centre, 2 right */
static void _a4_text(BITMAP* bmp, const FONT* f, const char* s, int x, int y, int color, int bg, int align) {
    ALLEGRO_FONT* af = _a4_font(f);
    if (!af || !s) return;
    int w = al_get_text_width(af, s);
    int left = align == 1 ? x - w / 2 : (align == 2 ? x - w : x);
    if (bg != -1) {
        int mode = a4_draw_mode;
        a4_draw_mode = DRAW_MODE_SOLID;
        rectfill(bmp, left, y, left + w - 1, y + al_get_font_line_height(af) - 1, bg);
        a4_draw_mode = mode;
    }
    if (!_a4_target(bmp)) return;
    /* font glyphs are premultiplied */
    al_set_separate_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA, ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
    ALLEGRO_COLOR c = (color == -1) ? al_map_rgb(255, 255, 255) : _a4_rgba(color, 255);
    al_draw_text(af, c, (float)left, (float)y, ALLEGRO_ALIGN_LEFT, s);
    _a4_done(bmp);
}

void textout_ex(BITMAP* bmp, const FONT* f, const char* s, int x, int y, int color, int bg) {
    _a4_text(bmp, f, s, x, y, color, bg, 0);
}

void textout_centre_ex(BITMAP* bmp, const FONT* f, const char* s, int x, int y, int color, int bg) {
    _a4_text(bmp, f, s, x, y, color, bg, 1);
}

void textout_right_ex(BITMAP* bmp, const FONT* f, const char* s, int x, int y, int color, int bg) {
    _a4_text(bmp, f, s, x, y, color, bg, 2);
}

/* the format arguments come straight from Allegro 4 callers */
#define A4_TEXTPRINTF(align)                 \
    char buf[1024];                          \
    va_list ap;                              \
    va_start(ap, format);                    \
    vsnprintf(buf, sizeof(buf), format, ap); \
    va_end(ap);                              \
    _a4_text(bmp, f, buf, x, y, color, bg, align);

void textprintf_ex(BITMAP* bmp, const FONT* f, int x, int y, int color, int bg, const char* format, ...) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
    A4_TEXTPRINTF(0)
#pragma GCC diagnostic pop
}

void textprintf_centre_ex(BITMAP* bmp, const FONT* f, int x, int y, int color, int bg, const char* format, ...) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
    A4_TEXTPRINTF(1)
#pragma GCC diagnostic pop
}

void textprintf_right_ex(BITMAP* bmp, const FONT* f, int x, int y, int color, int bg, const char* format, ...){
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
    A4_TEXTPRINTF(2)
#pragma GCC diagnostic pop
}

FONT* load_ttf_font(const char* filename, int points, int smooth) {
    char* path = _a4_resolve(filename);
    if (!path) return NULL;
    ALLEGRO_FONT* af = al_load_ttf_font(path, points, smooth == ALLEGTTF_NOSMOOTH ? ALLEGRO_TTF_MONOCHROME : 0);
    if (!af) {
        snprintf(allegro_error, sizeof(allegro_error), "could not load font %s", path);
        n_log(LOG_ERR, "could not load font %s", path);
        free(path);
        return NULL;
    }
    free(path);
    FONT* f = NULL;
    Malloc(f, FONT, 1);
    if (!f) {
        al_destroy_font(af);
        return NULL;
    }
    f->f = af;
    return f;
}

FONT* load_ttf_font_mem(const char* filename, int points, int smooth) {
    return load_ttf_font(filename, points, smooth);
}

void destroy_font(FONT* f) {
    if (!f || f == font) return;
    if (f->f) al_destroy_font(f->f);
    Free(f);
}

/* Message box. */

void allegro_message(const char* format, ...) {
    char msg[4096];
    va_list ap;
    va_start(ap, format);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
    vsnprintf(msg, sizeof(msg), format, ap);
#pragma GCC diagnostic pop
    va_end(ap);
    fprintf(stderr, "%s\n", msg);
    if (!a4_display || !screen || !font) return;

    /* split into lines, wrapping long ones */
    ALLEGRO_FONT* af = font->f;
    int lh = al_get_font_line_height(af) + 4;
    int maxw = screen->w - 60;
    char* lines[128];
    int nb = 0;
    for (char* p = msg; *p && nb < 128;) {
        char* end = strchr(p, '\n');
        size_t n = end ? (size_t)(end - p) : strlen(p);
        while (n > 0 && nb < 128) {
            size_t take = n;
            char tmp[1024];
            for (;;) {
                size_t c = take < sizeof(tmp) - 1 ? take : sizeof(tmp) - 1;
                memcpy(tmp, p, c);
                tmp[c] = '\0';
                if (al_get_text_width(af, tmp) <= maxw || take <= 1) break;
                take--;
            }
            lines[nb] = _a4_strndup(p, take);
            if (!lines[nb]) break;
            nb++;
            p += take;
            n -= take;
        }
        if (!end) break;
        if (n == 0 && end == p) {
            if (nb < 128) lines[nb++] = _a4_strndup("", 0);
        }
        p = end + 1;
    }

    int bw = 0;
    for (int i = 0; i < nb; i++) {
        int w = lines[i] ? al_get_text_width(af, lines[i]) : 0;
        if (w > bw) bw = w;
    }
    const char* hint = "Press a key or click to continue";
    if (al_get_text_width(af, hint) > bw) bw = al_get_text_width(af, hint);
    bw += 40;
    int bh = (nb + 2) * lh + 30;
    int bx = (screen->w - bw) / 2;
    int by = (screen->h - bh) / 2;

    BITMAP* saved = create_bitmap(screen->w, screen->h);
    if (saved) blit(screen, saved, 0, 0, 0, 0, screen->w, screen->h);
    int mode = a4_draw_mode;
    a4_draw_mode = DRAW_MODE_SOLID;
    int gl = a4_gl_mode;
    a4_gl_mode = 1;
    rectfill(screen, bx, by, bx + bw - 1, by + bh - 1, makecol(32, 32, 48));
    rect(screen, bx, by, bx + bw - 1, by + bh - 1, makecol(200, 200, 220));
    for (int i = 0; i < nb; i++) {
        if (lines[i]) textout_ex(screen, font, lines[i], bx + 20, by + 15 + i * lh, makecol(255, 255, 255), -1);
    }
    textout_centre_ex(screen, font, hint, bx + bw / 2, by + bh - 15 - lh, makecol(160, 160, 190), -1);
    _a4_present();

    /* wait for everything to be released, then for a new key or click */
    while ((mouse_b || keypressed()) && !a4_close_requested) {
        clear_keybuf();
        al_rest(0.01);
    }
    int mx = 0, my = 0;
    _a4_take_click(&mx, &my);
    while (!keypressed() && !mouse_b && !_a4_take_click(&mx, &my) && !a4_close_requested) al_rest(0.01);
    while (mouse_b && !a4_close_requested) al_rest(0.01);
    clear_keybuf();
    for (int i = 0; i < KEY_MAX; i++) key[i] = 0;

    if (saved) {
        blit(saved, screen, 0, 0, 0, 0, screen->w, screen->h);
        destroy_bitmap(saved);
    }
    _a4_present();
    a4_gl_mode = gl;
    a4_draw_mode = mode;
    for (int i = 0; i < nb; i++) free(lines[i]);
}

int alert(const char* s1, const char* s2, const char* s3, const char* b1, const char* b2, int c1, int c2) {
    const char* lines[3] = {s1, s2, s3};
    if (!a4_display || !screen || !font) {
        for (int i = 0; i < 3; i++)
            if (lines[i]) fprintf(stderr, "%s\n", lines[i]);
        return 1;
    }
    ALLEGRO_FONT* af = font->f;
    int lh = al_get_font_line_height(af) + 6;
    int nb_buttons = b2 ? 2 : 1;
    const char* labels[2] = {b1 ? b1 : "OK", b2};
    int bw = 0;
    for (int i = 0; i < 3; i++)
        if (lines[i] && al_get_text_width(af, lines[i]) > bw) bw = al_get_text_width(af, lines[i]);
    int btn_w = 0;
    for (int i = 0; i < nb_buttons; i++)
        if (al_get_text_width(af, labels[i]) + 24 > btn_w) btn_w = al_get_text_width(af, labels[i]) + 24;
    if (nb_buttons * btn_w + 20 > bw) bw = nb_buttons * btn_w + 20;
    bw += 40;
    int bh = 5 * lh + 30;
    int bx = (screen->w - bw) / 2, by = (screen->h - bh) / 2;
    int btn_y = by + bh - lh - 20;
    int btn_x[2];
    for (int i = 0; i < nb_buttons; i++) btn_x[i] = bx + (bw - nb_buttons * btn_w - (nb_buttons - 1) * 20) / 2 + i * (btn_w + 20);

    BITMAP* saved = create_bitmap(screen->w, screen->h);
    if (saved) blit(screen, saved, 0, 0, 0, 0, screen->w, screen->h);
    int mode = a4_draw_mode, gl = a4_gl_mode, cursor = a4_cursor_shown;
    a4_draw_mode = DRAW_MODE_SOLID;
    a4_gl_mode = 1;
    show_mouse(screen);
    while ((mouse_b || keypressed()) && !a4_close_requested) {
        clear_keybuf();
        al_rest(0.01);
    }
    int cx = 0, cy = 0;
    _a4_take_click(&cx, &cy);
    int choice = 0;
    while (!choice) {
        if (a4_close_requested) {
            choice = nb_buttons;
            break;
        }
        int over = -1;
        for (int i = 0; i < nb_buttons; i++)
            if (mouse_x >= btn_x[i] && mouse_x < btn_x[i] + btn_w && mouse_y >= btn_y && mouse_y < btn_y + lh + 6) over = i;
        rectfill(screen, bx, by, bx + bw - 1, by + bh - 1, makecol(32, 32, 48));
        rect(screen, bx, by, bx + bw - 1, by + bh - 1, makecol(200, 200, 220));
        for (int i = 0; i < 3; i++)
            if (lines[i]) textout_centre_ex(screen, font, lines[i], bx + bw / 2, by + 15 + i * lh, makecol(255, 255, 255), -1);
        for (int i = 0; i < nb_buttons; i++) {
            rectfill(screen, btn_x[i], btn_y, btn_x[i] + btn_w - 1, btn_y + lh + 5, i == over ? makecol(90, 90, 140) : makecol(60, 60, 90));
            rect(screen, btn_x[i], btn_y, btn_x[i] + btn_w - 1, btn_y + lh + 5, makecol(200, 200, 220));
            textout_centre_ex(screen, font, labels[i], btn_x[i] + btn_w / 2, btn_y + 6, makecol(255, 255, 255), -1);
        }
        _a4_present();
        while (keypressed()) {
            int k = readkey();
            int ascii = k & 0xFF;
            if (c1 && ascii && (ascii == c1 || ascii == (c1 ^ 0x20))) choice = 1;
            if (nb_buttons > 1 && c2 && ascii && (ascii == c2 || ascii == (c2 ^ 0x20))) choice = 2;
            if ((k >> 8) == KEY_ENTER || (k >> 8) == KEY_ENTER_PAD) choice = 1;
            if ((k >> 8) == KEY_ESC) choice = nb_buttons;
        }
        if (_a4_take_click(&cx, &cy)) {
            for (int i = 0; i < nb_buttons; i++)
                if (cx >= btn_x[i] && cx < btn_x[i] + btn_w && cy >= btn_y && cy < btn_y + lh + 6) choice = i + 1;
            if (choice)
                while ((mouse_b & 1) && !a4_close_requested) al_rest(0.01);
        }
        al_rest(0.01);
    }
    while ((key[KEY_ESC] || key[KEY_ENTER]) && !a4_close_requested) al_rest(0.01);
    clear_keybuf();
    if (saved) {
        blit(saved, screen, 0, 0, 0, 0, screen->w, screen->h);
        destroy_bitmap(saved);
    }
    _a4_present();
    if (!cursor) show_mouse(NULL);
    a4_gl_mode = gl;
    a4_draw_mode = mode;
    return choice;
}

/* Samples. */

int install_sound(int digi, int midi, const char* cfg_path) {
    (void)midi;
    (void)cfg_path;
    if (digi == DIGI_NONE) return 0;
    if (!a4_inited && allegro_init() != 0) return -1;
    if (a4_sound_ok) return 0;
    if (!al_install_audio()) {
        snprintf(allegro_error, sizeof(allegro_error), "could not start the audio");
        return -1;
    }
    al_init_acodec_addon();
    if (!al_reserve_samples(32)) {
        snprintf(allegro_error, sizeof(allegro_error), "could not reserve audio samples");
        return -1;
    }
    a4_sound_ok = 1;
    return 0;
}

void remove_sound(void) {
    if (!a4_sound_ok) return;
    _a4_midi_shutdown();
    al_stop_samples();
    al_uninstall_audio();
    a4_sound_ok = 0;
}

SAMPLE* load_sample(const char* filename) {
    if (!a4_sound_ok) return NULL;
    char* path = _a4_resolve(filename);
    if (!path) return NULL;
    ALLEGRO_SAMPLE* s = al_load_sample(path);
    if (!s) {
        n_log(LOG_ERR, "could not load sample %s", path);
        free(path);
        return NULL;
    }
    free(path);
    SAMPLE* spl = NULL;
    Malloc(spl, SAMPLE, 1);
    if (!spl) {
        al_destroy_sample(s);
        return NULL;
    }
    spl->spl = s;
    return spl;
}

void stop_sample(const SAMPLE* spl) {
    if (!spl || !a4_sound_ok) return;
    SAMPLE* s = (SAMPLE*)spl;
    for (int i = 0; i < A4_SAMPLE_IDS; i++) {
        if (s->used[i]) {
            al_stop_sample(&s->ids[i]);
            s->used[i] = 0;
        }
    }
}

void destroy_sample(SAMPLE* spl) {
    if (!spl) return;
    stop_sample(spl);
    if (spl->spl) al_destroy_sample(spl->spl);
    Free(spl);
}

static float _a4_gain(int vol) {
    return ((float)vol / 255.0f) * ((float)a4_digi_volume / 255.0f);
}

static float _a4_pan(int pan) {
    float p = ((float)pan - 128.0f) / 127.0f;
    return p < -1.0f ? -1.0f : (p > 1.0f ? 1.0f : p);
}

int play_sample(const SAMPLE* spl, int vol, int pan, int freq, int loop) {
    if (!spl || !a4_sound_ok) return -1;
    SAMPLE* s = (SAMPLE*)spl;
    ALLEGRO_SAMPLE_ID id;
    if (!al_play_sample(s->spl, _a4_gain(vol), _a4_pan(pan), (float)freq / 1000.0f, loop ? ALLEGRO_PLAYMODE_LOOP : ALLEGRO_PLAYMODE_ONCE, &id)) return -1;
    int slot = s->next;
    s->next = (s->next + 1) % A4_SAMPLE_IDS;
    s->ids[slot] = id;
    s->used[slot] = 1;
    return slot;
}

void adjust_sample(const SAMPLE* spl, int vol, int pan, int freq, int loop) {
    if (!spl || !a4_sound_ok) return;
    SAMPLE* s = (SAMPLE*)spl;
    for (int i = 0; i < A4_SAMPLE_IDS; i++) {
        if (!s->used[i]) continue;
        ALLEGRO_SAMPLE_INSTANCE* inst = al_lock_sample_id(&s->ids[i]);
        if (inst) {
            al_set_sample_instance_gain(inst, _a4_gain(vol));
            al_set_sample_instance_pan(inst, _a4_pan(pan));
            al_set_sample_instance_speed(inst, (float)freq / 1000.0f);
            al_set_sample_instance_playmode(inst, loop ? ALLEGRO_PLAYMODE_LOOP : ALLEGRO_PLAYMODE_ONCE);
        }
        al_unlock_sample_id(&s->ids[i]);
    }
}

/* MIDI through libfluidsynth, loaded at run time. */

typedef void* (*fs_new_settings_f)(void);
typedef int (*fs_settings_setnum_f)(void*, const char*, double);
typedef int (*fs_settings_setstr_f)(void*, const char*, const char*);
typedef void* (*fs_new_synth_f)(void*);
typedef int (*fs_sfload_f)(void*, const char*, int);
typedef void* (*fs_new_player_f)(void*);
typedef int (*fs_player_add_f)(void*, const char*);
typedef int (*fs_player_int_f)(void*);
typedef int (*fs_player_set_loop_f)(void*, int);
typedef int (*fs_write_float_f)(void*, int, void*, int, int, void*, int, int);
typedef void (*fs_delete_f)(void*);
typedef void* (*fs_set_log_function_f)(int, void*, void*);

static struct {
    int tried;
    int ok;
    N_MODULE* lib;
    fs_new_settings_f new_settings;
    fs_settings_setnum_f setnum;
    fs_settings_setstr_f setstr;
    fs_new_synth_f new_synth;
    fs_sfload_f sfload;
    fs_new_player_f new_player;
    fs_player_add_f player_add;
    fs_player_int_f player_play;
    fs_player_int_f player_stop;
    fs_player_set_loop_f player_set_loop;
    fs_write_float_f write_float;
    fs_player_int_f system_reset;
    fs_delete_f delete_player;
    fs_delete_f delete_synth;
    fs_delete_f delete_settings;
    fs_set_log_function_f set_log_function;
    void* settings;
    void* synth;
    void* player;
    MIDI* current;
    int paused;
    ALLEGRO_AUDIO_STREAM* stream;
    ALLEGRO_EVENT_QUEUE* queue;
    ALLEGRO_THREAD* thread;
    ALLEGRO_MUTEX* mutex;
} a4_fs;

#define A4_MIDI_RATE 44100
#define A4_MIDI_FRAG 1024

static N_MODULE* _a4_fluidsynth_open(void) {
    static const char* names[] = {
#if defined(__windows__)
        "libfluidsynth-3.dll", "libfluidsynth-2.dll", "fluidsynth.dll"
#elif defined(__APPLE__)
        "libfluidsynth.3.dylib", "libfluidsynth.dylib"
#else
        "libfluidsynth.so.3", "libfluidsynth.so.2", "libfluidsynth.so"
#endif
    };
    for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
        N_MODULE* m = n_module_open(names[i]);
        if (m) return m;
    }
    return NULL;
}

/* A symbol into a function pointer: memcpy, as n_module_symbol() advises. */
static int _a4_fluidsynth_sym(N_MODULE* lib, const char* name, void* fptr, size_t fsize) {
    void* p = n_module_symbol(lib, name);
    if (!p || fsize != sizeof(p)) return FALSE;
    memcpy(fptr, &p, fsize);
    return TRUE;
}

#define A4_SYM(field, name) ok = ok && _a4_fluidsynth_sym(a4_fs.lib, name, &a4_fs.field, sizeof(a4_fs.field))

static const char* _a4_soundfont(void) {
    static const char* candidates[] = {
        "/usr/share/sounds/sf2/default-GM.sf2",
        "/usr/share/sounds/sf2/FluidR3_GM.sf2",
        "/usr/share/sounds/sf2/TimGM6mb.sf2",
        "/usr/share/soundfonts/default.sf2",
        "/usr/share/soundfonts/FluidR3_GM.sf2",
        "/usr/local/share/soundfonts/default.sf2",
        "default.sf2"};
    const char* env = getenv("NILOREA_SOUNDFONT");
    if (env && env[0]) return env;
    for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
        FILE* f = fopen(candidates[i], "rb");
        if (f) {
            fclose(f);
            return candidates[i];
        }
    }
    return NULL;
}

static void* _a4_midi_proc(ALLEGRO_THREAD* thr, void* arg) {
    (void)arg;
    while (!al_get_thread_should_stop(thr)) {
        ALLEGRO_EVENT ev;
        if (!al_wait_for_event_timed(a4_fs.queue, &ev, 0.05f)) continue;
        if (ev.type != ALLEGRO_EVENT_AUDIO_STREAM_FRAGMENT) continue;
        float* buf = al_get_audio_stream_fragment(a4_fs.stream);
        if (!buf) continue;
        al_lock_mutex(a4_fs.mutex);
        if (a4_fs.paused)
            memset(buf, 0, sizeof(float) * 2 * A4_MIDI_FRAG);
        else
            a4_fs.write_float(a4_fs.synth, A4_MIDI_FRAG, buf, 0, 2, buf, 1, 2);
        al_unlock_mutex(a4_fs.mutex);
        al_set_audio_stream_fragment(a4_fs.stream, buf);
    }
    return NULL;
}

/* Bring fluidsynth up on first use. FALSE leaves MIDI silent. */
static int _a4_midi_init(void) {
    if (a4_fs.tried) return a4_fs.ok;
    a4_fs.tried = 1;
    if (!a4_sound_ok) return FALSE;
    const char* sf = _a4_soundfont();
    if (!sf) {
        n_log(LOG_NOTICE, "no General MIDI soundfont found (set NILOREA_SOUNDFONT), MIDI is silent");
        return FALSE;
    }
    a4_fs.lib = _a4_fluidsynth_open();
    if (!a4_fs.lib) {
        n_log(LOG_NOTICE, "libfluidsynth not found, MIDI is silent");
        return FALSE;
    }
    int ok = TRUE;
    A4_SYM(new_settings, "new_fluid_settings");
    A4_SYM(setnum, "fluid_settings_setnum");
    A4_SYM(setstr, "fluid_settings_setstr");
    A4_SYM(new_synth, "new_fluid_synth");
    A4_SYM(sfload, "fluid_synth_sfload");
    A4_SYM(new_player, "new_fluid_player");
    A4_SYM(player_add, "fluid_player_add");
    A4_SYM(player_play, "fluid_player_play");
    A4_SYM(player_stop, "fluid_player_stop");
    A4_SYM(player_set_loop, "fluid_player_set_loop");
    A4_SYM(write_float, "fluid_synth_write_float");
    A4_SYM(system_reset, "fluid_synth_system_reset");
    A4_SYM(delete_player, "delete_fluid_player");
    A4_SYM(delete_synth, "delete_fluid_synth");
    A4_SYM(delete_settings, "delete_fluid_settings");
    if (!ok) {
        n_log(LOG_NOTICE, "libfluidsynth lacks an expected symbol, MIDI is silent");
        return FALSE;
    }
    /* optional: keep fluidsynth's warnings about audio drivers it is not
     * asked to use (FLUID_WARN 2, FLUID_INFO 3, FLUID_DBG 4) off stderr */
    if (_a4_fluidsynth_sym(a4_fs.lib, "fluid_set_log_function", &a4_fs.set_log_function, sizeof(a4_fs.set_log_function))) {
        for (int level = 2; level <= 4; level++) a4_fs.set_log_function(level, NULL, NULL);
    }
    a4_fs.settings = a4_fs.new_settings();
    if (!a4_fs.settings) return FALSE;
    a4_fs.setnum(a4_fs.settings, "synth.sample-rate", (double)A4_MIDI_RATE);
    a4_fs.setnum(a4_fs.settings, "synth.gain", 0.5);
    /* advance the player with the rendered samples, not a system timer */
    a4_fs.setstr(a4_fs.settings, "player.timing-source", "sample");
    a4_fs.synth = a4_fs.new_synth(a4_fs.settings);
    if (!a4_fs.synth) return FALSE;
    if (a4_fs.sfload(a4_fs.synth, sf, 1) < 0) {
        n_log(LOG_NOTICE, "could not load soundfont %s, MIDI is silent", sf);
        return FALSE;
    }
    a4_fs.mutex = al_create_mutex();
    a4_fs.queue = al_create_event_queue();
    a4_fs.stream = al_create_audio_stream(4, A4_MIDI_FRAG, A4_MIDI_RATE, ALLEGRO_AUDIO_DEPTH_FLOAT32, ALLEGRO_CHANNEL_CONF_2);
    if (!a4_fs.mutex || !a4_fs.queue || !a4_fs.stream) return FALSE;
    al_set_audio_stream_gain(a4_fs.stream, (float)a4_midi_volume / 255.0f);
    if (!al_attach_audio_stream_to_mixer(a4_fs.stream, al_get_default_mixer())) return FALSE;
    al_register_event_source(a4_fs.queue, al_get_audio_stream_event_source(a4_fs.stream));
    a4_fs.thread = al_create_thread(_a4_midi_proc, NULL);
    if (!a4_fs.thread) return FALSE;
    al_start_thread(a4_fs.thread);
    n_log(LOG_DEBUG, "MIDI through fluidsynth with soundfont %s", sf);
    a4_fs.ok = TRUE;
    return TRUE;
}

static void _a4_midi_stop_locked(void) {
    if (a4_fs.player) {
        a4_fs.player_stop(a4_fs.player);
        a4_fs.delete_player(a4_fs.player);
        a4_fs.player = NULL;
    }
    if (a4_fs.synth && a4_fs.ok) a4_fs.system_reset(a4_fs.synth);
    a4_fs.current = NULL;
    a4_fs.paused = 0;
}

static void _a4_midi_shutdown(void) {
    if (a4_fs.thread) {
        al_destroy_thread(a4_fs.thread);
        a4_fs.thread = NULL;
    }
    if (a4_fs.mutex) _a4_midi_stop_locked();
    if (a4_fs.stream) {
        al_destroy_audio_stream(a4_fs.stream);
        a4_fs.stream = NULL;
    }
    if (a4_fs.queue) {
        al_destroy_event_queue(a4_fs.queue);
        a4_fs.queue = NULL;
    }
    if (a4_fs.mutex) {
        al_destroy_mutex(a4_fs.mutex);
        a4_fs.mutex = NULL;
    }
    if (a4_fs.synth) a4_fs.delete_synth(a4_fs.synth);
    if (a4_fs.settings) a4_fs.delete_settings(a4_fs.settings);
    a4_fs.synth = NULL;
    a4_fs.settings = NULL;
    n_module_close(&a4_fs.lib);
    memset(&a4_fs, 0, sizeof(a4_fs));
}

MIDI* load_midi(const char* filename) {
    char* path = _a4_resolve(filename);
    if (!path) return NULL;
    FILE* f = fopen(path, "rb");
    if (!f) {
        n_log(LOG_ERR, "could not open MIDI file %s", path);
        free(path);
        return NULL;
    }
    fclose(f);
    MIDI* m = NULL;
    Malloc(m, MIDI, 1);
    if (!m) {
        free(path);
        return NULL;
    }
    m->path = path;
    return m;
}

int play_midi(MIDI* midi, int loop) {
    if (!midi) {
        stop_midi();
        return 0;
    }
    if (!_a4_midi_init()) return 0;
    al_lock_mutex(a4_fs.mutex);
    _a4_midi_stop_locked();
    a4_fs.player = a4_fs.new_player(a4_fs.synth);
    if (a4_fs.player) {
        a4_fs.player_add(a4_fs.player, midi->path);
        a4_fs.player_set_loop(a4_fs.player, loop ? -1 : 1);
        a4_fs.player_play(a4_fs.player);
        a4_fs.current = midi;
    }
    al_unlock_mutex(a4_fs.mutex);
    return 0;
}

void stop_midi(void) {
    if (!a4_fs.ok) return;
    al_lock_mutex(a4_fs.mutex);
    _a4_midi_stop_locked();
    al_unlock_mutex(a4_fs.mutex);
}

void destroy_midi(MIDI* midi) {
    if (!midi) return;
    if (a4_fs.ok && a4_fs.current == midi) stop_midi();
    free(midi->path);
    Free(midi);
}

void midi_pause(void) {
    if (!a4_fs.ok) return;
    al_lock_mutex(a4_fs.mutex);
    a4_fs.paused = 1;
    al_unlock_mutex(a4_fs.mutex);
}

void midi_resume(void) {
    if (!a4_fs.ok) return;
    al_lock_mutex(a4_fs.mutex);
    a4_fs.paused = 0;
    al_unlock_mutex(a4_fs.mutex);
}

void set_volume(int digi_volume, int midi_volume) {
    if (digi_volume >= 0) a4_digi_volume = digi_volume > 255 ? 255 : digi_volume;
    if (midi_volume >= 0) a4_midi_volume = midi_volume > 255 ? 255 : midi_volume;
    if (a4_fs.stream) al_set_audio_stream_gain(a4_fs.stream, (float)a4_midi_volume / 255.0f);
}

/* Old nilorea Allegro 4 helpers. */

int gfx_mode(int mode, int w, int h, int v_w, int v_h, int depth, double r, double g, double b, double a) {
    (void)a;
    set_color_depth(depth);
    if (set_gfx_mode(mode, w, h, v_w, v_h) != 0) {
        allegro_message("Could not set a %dx%d graphics mode: %s", w, h, allegro_error);
        return FALSE;
    }
    /* components were given either as 0-1 or as 0-255 */
    double scale = (r > 1.0 || g > 1.0 || b > 1.0) ? 1.0 : 255.0;
    int gl = a4_gl_mode;
    a4_gl_mode = 1;
    clear_to_color(screen, makecol((int)(r * scale), (int)(g * scale), (int)(b * scale)));
    a4_gl_mode = gl;
    _a4_present();
    return TRUE;
}

int gfx_status(int action, int value) {
    if (action == SET) a4_gl_mode = (value == MODE_OGL);
    return a4_gl_mode ? MODE_OGL : MODE_STD;
}

void fill_str(char* str, int c, size_t len) {
    if (str) memset(str, c, len);
}

int a4_get_keyboard(char* keybuf, int* cur, int flags, int max) {
    (void)flags;
    if (!keybuf || !cur || max <= 0) return 0;
    if (*cur < 0) *cur = 0;
    if (*cur > max - 1) *cur = max - 1;
    int taken = 0;
    while (keypressed()) {
        int k = readkey();
        int ascii = k & 0xFF;
        taken++;
        if ((k >> 8) == KEY_BACKSPACE) {
            if (*cur > 0) (*cur)--;
            keybuf[*cur] = '\0';
        } else if (ascii >= 32 && ascii < 127 && *cur < max - 1) {
            keybuf[(*cur)++] = (char)ascii;
            keybuf[*cur] = '\0';
        }
    }
    return taken;
}

/* Old nilorea INI driven menu. */

typedef struct LEX_ITEM {
    BITMAP* image;
    BITMAP* over;
    SAMPLE* sound;
    int x;
    int y;
    int h_align; /* 0 left, 1 centre, 2 right: which part of the image is at x */
    int v_align; /* 0 top, 1 middle, 2 bottom: which side of y the image hangs */
} LEX_ITEM;

struct LexMenu {
    BITMAP* bg;
    LEX_ITEM* items;
    int nb_items;
    int selected;
};

static int _a4_align(const char* v, int is_h) {
    if (!v) return 0;
    if (!strcasecmp(v, "center") || !strcasecmp(v, "centre") || !strcasecmp(v, "middle")) return 1;
    if (is_h && !strcasecmp(v, "right")) return 2;
    if (!is_h && !strcasecmp(v, "bottom")) return 2;
    return 0;
}

/* A brighter copy of an item image, shown under the pointer. */
static BITMAP* _a4_highlight(BITMAP* src) {
    BITMAP* b = create_bitmap(src->w, src->h);
    if (!b) return NULL;
    ALLEGRO_BITMAP* s = _a4_source(src, FALSE);
    _a4_target(b);
    al_clear_to_color(al_map_rgba(0, 0, 0, 0));
    _a4_blend_copy();
    al_draw_bitmap(s, 0, 0, 0);
    /* add 35% white where the image is not transparent */
    al_set_separate_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ONE, ALLEGRO_ADD, ALLEGRO_ZERO, ALLEGRO_ONE);
    al_draw_tinted_bitmap(s, al_map_rgba_f(0.35f, 0.35f, 0.35f, 1.0f), 0, 0, 0);
    return b;
}

LexMenu* lexLoadMenuFromFile(const char* file, const char* section) {
    if (!file || !section) return NULL;
    char* path = _a4_resolve(file);
    if (!path) return NULL;
    ALLEGRO_CONFIG* cfg = al_load_config_file(path);
    if (!cfg) {
        n_log(LOG_ERR, "could not load menu file %s", path);
        free(path);
        return NULL;
    }
    free(path);
    LexMenu* menu = NULL;
    Malloc(menu, LexMenu, 1);
    if (!menu) {
        al_destroy_config(cfg);
        return NULL;
    }
    const char* bg = al_get_config_value(cfg, section, "bg");
    if (bg) menu->bg = load_bitmap(bg, NULL);

    int count = 0;
    for (;;) {
        char k[32];
        snprintf(k, sizeof(k), "item%d", count);
        if (!al_get_config_value(cfg, section, k)) break;
        count++;
    }
    if (count > 0) Malloc(menu->items, LEX_ITEM, (size_t)count);
    if (!menu->items) count = 0;
    for (int i = 0; i < count; i++) {
        char k[32];
        char sec[512];
        snprintf(k, sizeof(k), "item%d", i);
        snprintf(sec, sizeof(sec), "%s.%s", section, al_get_config_value(cfg, section, k));
        LEX_ITEM* it = &menu->items[i];
        const char* v = al_get_config_value(cfg, sec, "x");
        it->x = v ? atoi(v) : 0;
        v = al_get_config_value(cfg, sec, "y");
        it->y = v ? atoi(v) : 0;
        it->h_align = _a4_align(al_get_config_value(cfg, sec, "h_align"), TRUE);
        it->v_align = _a4_align(al_get_config_value(cfg, sec, "v_align"), FALSE);
        v = al_get_config_value(cfg, sec, "image");
        if (v) it->image = load_bitmap(v, NULL);
        if (it->image) it->over = _a4_highlight(it->image);
        v = al_get_config_value(cfg, sec, "sound");
        if (v) it->sound = load_sample(v);
    }
    menu->nb_items = count;
    al_destroy_config(cfg);
    return menu;
}

BITMAP* lexMenuGetBitmap(LexMenu* menu, int item, int state) {
    if (!menu || item < 0 || item >= menu->nb_items) return NULL;
    return state == LEX_MENU_OVER ? menu->items[item].over : menu->items[item].image;
}

/* Rectangle of an item in buf coordinates. */
static void _a4_item_rect(const LexMenu* menu, const BITMAP* buf, int i, float* x, float* y, float* w, float* h) {
    const LEX_ITEM* it = &menu->items[i];
    float sx = menu->bg ? (float)buf->w / (float)menu->bg->w : 1.0f;
    float sy = menu->bg ? (float)buf->h / (float)menu->bg->h : 1.0f;
    float iw = it->image ? (float)it->image->w : 0.0f;
    float ih = it->image ? (float)it->image->h : 0.0f;
    float ix = (float)it->x;
    float iy = (float)it->y;
    /* h_align says which part of the image is at x. v_align says on which
     * side of y the image hangs: "bottom" puts it below y, so y is its top
     * edge. That is how the menu files of the games that used the old
     * module place their buttons (Elementals' last button ends exactly on
     * its panel's bottom edge, The Fallings' first one clears the byline). */
    if (it->h_align == 1) ix -= iw / 2.0f;
    if (it->h_align == 2) ix -= iw;
    if (it->v_align == 1) iy -= ih / 2.0f;
    if (it->v_align == 0) iy -= ih;
    *x = ix * sx;
    *y = iy * sy;
    *w = iw * sx;
    *h = ih * sy;
}

static int _a4_item_at(const LexMenu* menu, const BITMAP* buf, int mx, int my) {
    for (int i = 0; i < menu->nb_items; i++) {
        float x, y, w, h;
        _a4_item_rect(menu, buf, i, &x, &y, &w, &h);
        if ((float)mx >= x && (float)mx < x + w && (float)my >= y && (float)my < y + h) return i;
    }
    return -1;
}

int lexMenuRun(LexMenu* menu, BITMAP* buf) {
    if (!menu || !buf || menu->nb_items <= 0) return -1;
    int cursor = a4_cursor_shown;
    show_mouse(buf);
    /* do not take the click or key that opened the menu as a choice */
    while ((mouse_b || key[KEY_ESC] || key[KEY_ENTER] || key[KEY_SPACE]) && !a4_close_requested) al_rest(0.01);
    clear_keybuf();
    int cx = 0, cy = 0;
    _a4_take_click(&cx, &cy);
    if (menu->selected < 0 || menu->selected >= menu->nb_items) menu->selected = 0;
    int chosen = -2;
    int last_mx = mouse_x, last_my = mouse_y;
    while (chosen == -2) {
        if (a4_close_requested) {
            chosen = -1;
            break;
        }
        int hover = _a4_item_at(menu, buf, mouse_x, mouse_y);
        if (hover >= 0 && (mouse_x != last_mx || mouse_y != last_my)) menu->selected = hover;
        last_mx = mouse_x;
        last_my = mouse_y;
        while (keypressed()) {
            int k = readkey() >> 8;
            if (k == KEY_UP) menu->selected = (menu->selected + menu->nb_items - 1) % menu->nb_items;
            if (k == KEY_DOWN) menu->selected = (menu->selected + 1) % menu->nb_items;
            if (k == KEY_ENTER || k == KEY_ENTER_PAD || k == KEY_SPACE) chosen = menu->selected;
            if (k == KEY_ESC) chosen = -1;
        }
        if (_a4_take_click(&cx, &cy)) {
            int clicked = _a4_item_at(menu, buf, cx, cy);
            if (clicked >= 0) {
                menu->selected = clicked;
                chosen = clicked;
                while ((mouse_b & 1) && !a4_close_requested) al_rest(0.01);
            }
        }

        /* draw */
        if (menu->bg)
            stretch_blit(menu->bg, buf, 0, 0, menu->bg->w, menu->bg->h, 0, 0, buf->w, buf->h);
        else
            clear(buf);
        for (int i = 0; i < menu->nb_items; i++) {
            BITMAP* img = (i == menu->selected && menu->items[i].over) ? menu->items[i].over : menu->items[i].image;
            if (!img) continue;
            float x, y, w, h;
            _a4_item_rect(menu, buf, i, &x, &y, &w, &h);
            ALLEGRO_BITMAP* s = _a4_source(img, TRUE);
            _a4_target(buf);
            _a4_blend_alpha();
            al_draw_scaled_bitmap(s, 0, 0, (float)img->w, (float)img->h, x, y, w, h, 0);
        }
        if (screen) {
            int gl = a4_gl_mode;
            a4_gl_mode = 1;
            stretch_blit(buf, screen, 0, 0, buf->w, buf->h, 0, 0, screen->w, screen->h);
            _a4_present();
            a4_gl_mode = gl;
        }
        al_rest(0.01);
    }
    if (chosen >= 0 && menu->items[chosen].sound) play_sample(menu->items[chosen].sound, 255, 128, 1000, 0);
    while (key[KEY_ESC] && !a4_close_requested) al_rest(0.01);
    clear_keybuf();
    if (!cursor) show_mouse(NULL);
    return chosen;
}

LexMenu* lexMenuDestroy(LexMenu* menu) {
    if (!menu) return NULL;
    for (int i = 0; i < menu->nb_items; i++) {
        destroy_bitmap(menu->items[i].image);
        destroy_bitmap(menu->items[i].over);
        destroy_sample(menu->items[i].sound);
    }
    Free(menu->items);
    destroy_bitmap(menu->bg);
    Free(menu);
    return NULL;
}