Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d598b854c | |||
| 492cee4373 | |||
| a0c53e381c | |||
| 46d7e1263c | |||
| 426607f916 | |||
| 5badcb6143 | |||
| 7ebd996eed | |||
| e89b329b54 | |||
| d490cfa8f4 | |||
| abf0d8ca78 | |||
| 7f7022b960 | |||
| 3a63e14399 | |||
| 99ac826a49 | |||
| a3698f93a9 | |||
| 94dcc82483 |
@@ -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);
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
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);
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
100–2500 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.",
|
||||
)
|
||||
|
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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
|
After Width: | Height: | Size: 671 B |
|
After Width: | Height: | Size: 1016 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 287 B |
@@ -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 `0–4`. `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 `0–4` 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), 0–32 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 1–2 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.",
|
||||
)
|
||||
|
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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
@@ -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 ®) {
|
||||
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));
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 */
|
||||
}
|
||||