joeylib2/src/amiga/input.c

486 lines
18 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Amiga keyboard + mouse input: opens a backdrop borderless window on
// the JoeyLib CUSTOMSCREEN to receive IDCMP_RAWKEY, IDCMP_MOUSEMOVE,
// and IDCMP_MOUSEBUTTONS events, then drains those messages each
// jlInputPoll.
//
// RAWKEY is the lowest-level IDCMP event Intuition exposes: ie_Code
// carries the Amiga raw key code (press in 0x00..0x7F, release with
// bit 7 set), which we map straight to jlKeyE entries. The backdrop
// borderless window covers the whole screen without drawing over our
// planar bitmap (c2p writes to Screen->BitMap->Planes directly), and
// WFLG_ACTIVATE ensures keyboard focus lands on us from the start.
//
// MOUSEMOVE messages carry MouseX/MouseY in window-relative pixels;
// since the window is at (0,0) covering the whole screen, those are
// also surface-space coordinates. WA_ReportMouse = TRUE is required
// for MOUSEMOVE events to fire continuously rather than only on button
// transitions. WFLG_RMBTRAP keeps the right mouse button from opening
// Intuition's screen menu so right-clicks come to us as MENUDOWN.
//
// Typed characters: RAWKEY press events are also run through
// keymap.library's MapRawKey with the message qualifier and dead-key
// context, which applies the user's keymap (shift, caps, national
// layouts, dead keys) and yields translated bytes for the core typed-
// character queue. Intuition delivers key repeats as further RAWKEY
// events, so OS auto-repeat produces repeated characters for free.
#include <string.h>
#include <exec/types.h>
#include <exec/ports.h>
#include <intuition/intuition.h>
#include <intuition/screens.h>
#include <devices/inputevent.h>
#include <libraries/lowlevel.h>
#include <proto/exec.h>
#include <proto/intuition.h>
#include <proto/keymap.h>
#include <proto/lowlevel.h>
#include "port.h"
#include "inputInternal.h"
// ----- Constants -----
#define AMIGA_KEY_TABLE_SIZE 128
#define AMIGA_KEY_RELEASE_BIT 0x80
#define AMIGA_KEY_CODE_MASK 0x7F
// MapRawKey output buffer. A single keystroke maps to at most a few
// bytes (dead-key compositions); 8 is comfortably above any keymap's
// output.
#define MAP_RAW_KEY_BUF_SIZE 8
// Keymaps encode arrows / F-keys / Help as ANSI control sequences
// beginning with CSI (0x9B). The CSI byte itself would be filtered,
// but the sequence's TAIL bytes are printable ('A' for cursor up,
// "0~" for F1, ...) and would leak into the typed-character queue as
// stray text -- so a mapping that starts with CSI is discarded whole.
#define AMIGA_CSI 0x9B
// keymap.library base referenced by the MapRawKey inline stub. Defined
// here and opened explicitly (not left to libnix auto-open) so a
// system without the library degrades to "no typed characters" instead
// of failing at startup.
struct Library *KeymapBase = NULL;
// lowlevel.library base referenced by the ReadJoyPort and SetJoyPortAttrs
// inline stubs, defined and opened here for the same reason: a boot drive
// without Workbench's Libs/lowlevel.library degrades to "no joysticks".
struct Library *LowLevelBase = NULL;
// ----- External from hal.c -----
extern struct Screen *jlgScreen;
// ----- Empty pointer image -----
//
// Full-screen custom-screen games conventionally hide the system
// pointer; we install an all-zero sprite via SetPointer so Workbench's
// arrow doesn't drift over the playfield. Game code that needs a
// cursor draws its own from jlMouseX/Y.
//
// Amiga hardware sprite data: 2 leading control words (VSTART/HSTART/
// VSTOP), one line of (planeA, planeB) data words, 2 trailing zero
// words as terminator. All zeros yields a transparent pointer. Must
// live in chip RAM since sprite DMA reads it.
#define EMPTY_POINTER_BYTES (6 * sizeof(UWORD))
static UWORD *gEmptyPointer = NULL;
// ----- Prototypes -----
static void drainMessages(void);
static void joystickUnlock(int stick);
static void mouseButtonEvent(jlMouseButtonE button, bool down);
static void pollJoysticks(void);
// ----- Module state -----
// Amiga raw-key code -> jlKeyE. Codes are layout-independent; the
// OS has a separate keymap for producing characters. A second key for a
// named key carries INPUT_KEY_SECOND.
static const uint8_t gRawKeyToKey[AMIGA_KEY_TABLE_SIZE] = {
[0x01] = JL_KEY_1,
[0x02] = JL_KEY_2,
[0x03] = JL_KEY_3,
[0x04] = JL_KEY_4,
[0x05] = JL_KEY_5,
[0x06] = JL_KEY_6,
[0x07] = JL_KEY_7,
[0x08] = JL_KEY_8,
[0x09] = JL_KEY_9,
[0x0A] = JL_KEY_0,
[0x10] = JL_KEY_Q,
[0x11] = JL_KEY_W,
[0x12] = JL_KEY_E,
[0x13] = JL_KEY_R,
[0x14] = JL_KEY_T,
[0x15] = JL_KEY_Y,
[0x16] = JL_KEY_U,
[0x17] = JL_KEY_I,
[0x18] = JL_KEY_O,
[0x19] = JL_KEY_P,
[0x1E] = JL_KEY_DOWN | INPUT_KEY_SECOND, // keypad 2, 4, 6 and 8: the arrows too,
[0x20] = JL_KEY_A,
[0x21] = JL_KEY_S,
[0x22] = JL_KEY_D,
[0x23] = JL_KEY_F,
[0x24] = JL_KEY_G,
[0x25] = JL_KEY_H,
[0x26] = JL_KEY_J,
[0x27] = JL_KEY_K,
[0x28] = JL_KEY_L,
[0x2D] = JL_KEY_LEFT | INPUT_KEY_SECOND, // as on every port with a keypad
[0x2F] = JL_KEY_RIGHT | INPUT_KEY_SECOND,
[0x31] = JL_KEY_Z,
[0x32] = JL_KEY_X,
[0x33] = JL_KEY_C,
[0x34] = JL_KEY_V,
[0x35] = JL_KEY_B,
[0x36] = JL_KEY_N,
[0x37] = JL_KEY_M,
[0x3E] = JL_KEY_UP | INPUT_KEY_SECOND,
[0x40] = JL_KEY_SPACE,
[0x41] = JL_KEY_BACKSPACE,
[0x42] = JL_KEY_TAB,
[0x43] = JL_KEY_RETURN | INPUT_KEY_SECOND, // keypad Enter, as every port reads it
[0x44] = JL_KEY_RETURN,
[0x45] = JL_KEY_ESCAPE,
[0x4C] = JL_KEY_UP,
[0x4D] = JL_KEY_DOWN,
[0x4E] = JL_KEY_RIGHT,
[0x4F] = JL_KEY_LEFT,
[0x50] = JL_KEY_F1,
[0x51] = JL_KEY_F2,
[0x52] = JL_KEY_F3,
[0x53] = JL_KEY_F4,
[0x54] = JL_KEY_F5,
[0x55] = JL_KEY_F6,
[0x56] = JL_KEY_F7,
[0x57] = JL_KEY_F8,
[0x58] = JL_KEY_F9,
[0x59] = JL_KEY_F10,
[0x60] = JL_KEY_LSHIFT,
[0x61] = JL_KEY_RSHIFT,
[0x63] = JL_KEY_LCTRL,
[0x64] = JL_KEY_LALT,
[0x65] = JL_KEY_LALT | INPUT_KEY_SECOND, // right Alt: either Alt, as on the PC ports
};
static struct Window *gWindow = NULL;
// AmigaOS exposes joystick port 1 (the dedicated stick) via lowlevel
// ReadJoyPort(1); port 0 shares the mouse port. We map our public
// JL_JOYSTICK_0 to Amiga port 1 so the primary stick is what retro games
// expect.
static const ULONG gAmigaPortForStick[JL_JOYSTICK_COUNT] = { 1, 0 };
// lowlevel.library senses a port's controller again now and then, and a
// two-button stick held with both buttons down reads as no controller
// then, until it moves (measured under FS-UAE). So a port that has shown
// a stick is set to one (SetJoyPortAttrs) and stays one until
// jlJoystickReset or shutdown hands it back to autosensing.
static bool gJoyLocked[JL_JOYSTICK_COUNT];
// lowlevel.library reports a one-button stick and a two-button one alike
// (a second button is pin 9, which reads as released until it is pressed),
// and most Amiga owners use one-button Atari sticks. So a joystick counts as
// one-button (jlgJoyOneButton: a hold of its button is button 1) until its
// second button has been seen pressed, and again once its port has no
// controller. A CD32 game controller always has both.
static bool gJoySecondButton[JL_JOYSTICK_COUNT];
// Keys and mouse buttons as Intuition's messages leave them, and pressed
// since the last poll: a tap between two polls still reads down for one
// (jlInputLatch). The keys are latched only when a message changed them or
// the last latch showed a tap that must now go up.
static uint8_t gKeyHeld [JL_KEY_COUNT];
static uint8_t gKeyTapped [JL_KEY_COUNT];
static bool gKeysChanged;
static bool gKeyTapShown;
static uint8_t gMouseHeld [JL_MOUSE_BUTTON_COUNT];
static uint8_t gMouseTapped [JL_MOUSE_BUTTON_COUNT];
// ----- Internal helpers -----
static void drainMessages(void) {
struct IntuiMessage *msg;
struct InputEvent ie;
UBYTE mapBuf[MAP_RAW_KEY_BUF_SIZE];
WORD mapCount;
WORD mapIdx;
bool mapChord;
ULONG msgClass;
UWORD msgCode;
int16_t msgMouseX;
int16_t msgMouseY;
uint8_t code;
uint8_t entry;
uint8_t key;
bool isRelease;
if (gWindow == NULL) {
return;
}
while ((msg = (struct IntuiMessage *)GetMsg(gWindow->UserPort)) != NULL) {
msgClass = msg->Class;
msgCode = msg->Code;
msgMouseX = (int16_t)msg->MouseX;
msgMouseY = (int16_t)msg->MouseY;
// Typed-character translation must run BEFORE ReplyMsg: the
// qualifier and the dead-key context behind IAddress belong
// to the message and may be recycled once replied. Release
// events (bit 7) type nothing.
mapCount = 0;
mapChord = (msg->Qualifier & IEQUALIFIER_CONTROL) != 0;
if (msgClass == IDCMP_RAWKEY && (msgCode & AMIGA_KEY_RELEASE_BIT) == 0 && KeymapBase != NULL) {
ie.ie_NextEvent = NULL;
ie.ie_Class = IECLASS_RAWKEY;
ie.ie_SubClass = 0;
ie.ie_Code = msgCode;
ie.ie_Qualifier = msg->Qualifier;
// Dead-key context: IAddress points at the previous-code
// storage MapRawKey wants in ie_EventAddress (RKM recipe).
ie.ie_EventAddress = (APTR)*((ULONG *)msg->IAddress);
mapCount = MapRawKey(&ie, (STRPTR)mapBuf, MAP_RAW_KEY_BUF_SIZE, NULL);
if (mapCount > 0 && mapBuf[0] == AMIGA_CSI) {
mapCount = 0;
}
}
ReplyMsg((struct Message *)msg);
switch (msgClass) {
case IDCMP_RAWKEY:
isRelease = (msgCode & AMIGA_KEY_RELEASE_BIT) != 0;
code = (uint8_t)(msgCode & AMIGA_KEY_CODE_MASK);
entry = gRawKeyToKey[code];
key = (uint8_t)(entry & INPUT_KEY_MASK);
if (key != JL_KEY_NONE) {
gKeyHeld[key] = inputKeyHeld(gKeyHeld[key], entry, !isRelease);
if (!isRelease) {
gKeyTapped[key] = 1u;
}
gKeysChanged = true;
}
// MapRawKey returns -1 on overflow (composition longer
// than the buffer) -- nothing usable, skip.
// A Ctrl chord types nothing (jlInputCharTyped); Alt is a
// layer of the keymap's characters.
for (mapIdx = 0; mapIdx < mapCount; mapIdx++) {
jlInputCharTyped((uint8_t)mapBuf[mapIdx], mapChord);
}
break;
case IDCMP_MOUSEMOVE:
jlInputMouseMove(msgMouseX, msgMouseY);
break;
case IDCMP_INACTIVEWINDOW:
// The keys and buttons it lets go elsewhere are never
// reported here: let them all go now.
memset(gKeyHeld, 0, sizeof(gKeyHeld));
memset(gMouseHeld, 0, sizeof(gMouseHeld));
gKeysChanged = true;
break;
case IDCMP_MOUSEBUTTONS:
switch (msgCode) {
case SELECTDOWN:
mouseButtonEvent(JL_MOUSE_BUTTON_LEFT, true);
break;
case SELECTUP:
mouseButtonEvent(JL_MOUSE_BUTTON_LEFT, false);
break;
case MENUDOWN:
mouseButtonEvent(JL_MOUSE_BUTTON_RIGHT, true);
break;
case MENUUP:
mouseButtonEvent(JL_MOUSE_BUTTON_RIGHT, false);
break;
case MIDDLEDOWN:
mouseButtonEvent(JL_MOUSE_BUTTON_MIDDLE, true);
break;
case MIDDLEUP:
mouseButtonEvent(JL_MOUSE_BUTTON_MIDDLE, false);
break;
default:
break;
}
// MOUSEBUTTONS messages also carry the mouse position.
jlInputMouseMove(msgMouseX, msgMouseY);
break;
default:
break;
}
}
}
// Hand a locked port back to lowlevel.library's autosensing (which also
// frees the potgo bits it took for the stick).
static void joystickUnlock(int stick) {
if (!gJoyLocked[stick]) {
return;
}
SetJoyPortAttrs(gAmigaPortForStick[stick], SJA_Reinitialize, TRUE, TAG_DONE);
gJoyLocked[stick] = false;
}
// A mouse button's message: held as it leaves it, and pressed since the
// last poll.
static void mouseButtonEvent(jlMouseButtonE button, bool down) {
gMouseHeld[button] = down;
if (down) {
gMouseTapped[button] = 1u;
}
}
// Read both joystick ports via lowlevel.library. Devices that report
// JP_TYPE_JOYSTK or JP_TYPE_GAMECTLR populate gJoy*; anything else
// (mouse on port 0, no device, unknown) leaves the stick disconnected
// with zeroed state.
static void pollJoysticks(void) {
ULONG state;
ULONG type;
int stick;
if (LowLevelBase == NULL) {
return;
}
for (stick = 0; stick < JL_JOYSTICK_COUNT; stick++) {
state = ReadJoyPort(gAmigaPortForStick[stick]);
type = state & JP_TYPE_MASK;
if (type == JP_TYPE_JOYSTK && !gJoyLocked[stick]) {
gJoyLocked[stick] = SetJoyPortAttrs(gAmigaPortForStick[stick], SJA_Type, SJA_TYPE_JOYSTK, TAG_DONE) != FALSE;
}
if (type != JP_TYPE_JOYSTK && type != JP_TYPE_GAMECTLR) {
jlgJoyConnected[stick] = false;
jlgJoyAxisX[stick] = 0;
jlgJoyAxisY[stick] = 0;
jlgJoyOneButton[stick] = false;
gJoySecondButton[stick] = false;
jlgJoyButtonState[stick][JL_JOY_BUTTON_0] = false;
jlgJoyButtonState[stick][JL_JOY_BUTTON_1] = false;
continue;
}
jlgJoyConnected[stick] = true;
jlgJoyAxisX[stick] = 0;
if (state & JPF_JOY_LEFT) {
jlgJoyAxisX[stick] = JL_JOYSTICK_AXIS_MIN;
}
if (state & JPF_JOY_RIGHT) {
jlgJoyAxisX[stick] = JL_JOYSTICK_AXIS_MAX;
}
jlgJoyAxisY[stick] = 0;
if (state & JPF_JOY_UP) {
jlgJoyAxisY[stick] = JL_JOYSTICK_AXIS_MIN;
}
if (state & JPF_JOY_DOWN) {
jlgJoyAxisY[stick] = JL_JOYSTICK_AXIS_MAX;
}
// Standard 1-button stick reports JPF_BUTTON_RED only. CD32
// game controllers also report BLUE for the second face button.
jlgJoyButtonState[stick][JL_JOY_BUTTON_0] = (state & JPF_BUTTON_RED) != 0;
jlgJoyButtonState[stick][JL_JOY_BUTTON_1] = (state & JPF_BUTTON_BLUE) != 0;
if (type == JP_TYPE_GAMECTLR || (state & JPF_BUTTON_BLUE) != 0) {
gJoySecondButton[stick] = true;
}
jlgJoyOneButton[stick] = !gJoySecondButton[stick];
}
}
// ----- HAL API (alphabetical) -----
// Sticks are digital, so nothing to calibrate: the port goes back to
// autosensing, as a stick swapped for a mouse or a game controller needs.
void jlpJoystickReset(jlJoystickE js) {
joystickUnlock((int)js);
}
void jlpInputInit(void) {
memset(jlgKeyState, 0, sizeof(jlgKeyState));
memset(jlgKeyPrev, 0, sizeof(jlgKeyPrev));
memset(gKeyHeld, 0, sizeof(gKeyHeld));
memset(gKeyTapped, 0, sizeof(gKeyTapped));
gKeysChanged = true;
memset(gMouseHeld, 0, sizeof(gMouseHeld));
memset(gMouseTapped, 0, sizeof(gMouseTapped));
if (jlgScreen == NULL) {
return;
}
gWindow = OpenWindowTags(NULL, (ULONG)WA_CustomScreen, (ULONG)jlgScreen, (ULONG)WA_Left, (ULONG)0, (ULONG)WA_Top, (ULONG)0, (ULONG)WA_Width, (ULONG)jlgScreen->Width, (ULONG)WA_Height, (ULONG)jlgScreen->Height, (ULONG)WA_Flags, (ULONG)(WFLG_BACKDROP | WFLG_BORDERLESS | WFLG_ACTIVATE | WFLG_RMBTRAP | WFLG_NOCAREREFRESH | WFLG_REPORTMOUSE), (ULONG)WA_IDCMP, (ULONG)(IDCMP_RAWKEY | IDCMP_MOUSEMOVE | IDCMP_MOUSEBUTTONS | IDCMP_INACTIVEWINDOW), TAG_DONE);
if (gWindow != NULL) {
jlgMouseAttached = true; // Intuition delivers IDCMP mouse events on this window
gEmptyPointer = (UWORD *)AllocMem((ULONG)EMPTY_POINTER_BYTES, (ULONG)(MEMF_CHIP | MEMF_CLEAR));
if (gEmptyPointer != NULL) {
SetPointer(gWindow, gEmptyPointer, 1, 16, 0, 0);
}
}
// lowlevel.library (OS 3.1, V40) is a disk library: Kickstart has none
// in ROM. If the boot drive has none either, the joystick API silently
// reports disconnected sticks rather than failing init.
memset(gJoyLocked, 0, sizeof(gJoyLocked));
LowLevelBase = OpenLibrary((CONST_STRPTR)"lowlevel.library", 0);
// keymap.library (OS 2.0+) drives the typed-character path. If
// absent, jlInputGetChar just stays empty; the predicates still
// work off the raw codes.
KeymapBase = OpenLibrary((CONST_STRPTR)"keymap.library", 0);
}
void jlpInputPoll(void) {
drainMessages();
if (gKeysChanged || gKeyTapShown) {
gKeysChanged = false;
gKeyTapShown = jlInputLatch(jlgKeyState, gKeyHeld, gKeyTapped, JL_KEY_COUNT);
}
jlInputLatch(jlgMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT);
pollJoysticks();
}
void jlpInputShutdown(void) {
if (KeymapBase != NULL) {
CloseLibrary(KeymapBase);
KeymapBase = NULL;
}
if (LowLevelBase != NULL) {
// The ports' types are lowlevel.library's, shared with every
// program: none may stay set to a stick after this one.
joystickUnlock(JL_JOYSTICK_0);
joystickUnlock(JL_JOYSTICK_1);
CloseLibrary(LowLevelBase);
LowLevelBase = NULL;
}
if (gWindow == NULL) {
return;
}
drainMessages();
if (gEmptyPointer != NULL) {
// Restore the system pointer before freeing our image so
// Intuition isn't holding a stale chip-RAM reference.
ClearPointer(gWindow);
FreeMem(gEmptyPointer, (ULONG)EMPTY_POINTER_BYTES);
gEmptyPointer = NULL;
}
CloseWindow(gWindow);
gWindow = NULL;
}