Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0c53e381c | |||
| 46d7e1263c | |||
| 426607f916 | |||
| 5badcb6143 | |||
| 7ebd996eed | |||
| e89b329b54 | |||
| d490cfa8f4 | |||
| abf0d8ca78 | |||
| 7f7022b960 | |||
| 3a63e14399 | |||
| 99ac826a49 | |||
| a3698f93a9 | |||
| 94dcc82483 | |||
| 9b7499be36 | |||
| 018a5feb29 |
@@ -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
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
App(
|
||||
appid="rolljam",
|
||||
name="RollJam",
|
||||
apptype=FlipperAppType.MENUEXTERNAL,
|
||||
entry_point="rolljam_app",
|
||||
stack_size=4 * 1024,
|
||||
fap_category="Sub-GHz",
|
||||
fap_icon="rolljam.png",
|
||||
fap_icon_assets="images",
|
||||
fap_libs=["assets"],
|
||||
fap_description="RollJam rolling code attack tool",
|
||||
fap_author="@user",
|
||||
fap_version="1.0",
|
||||
fap_weburl="",
|
||||
requires=[
|
||||
"gui",
|
||||
"subghz",
|
||||
"notification",
|
||||
"storage",
|
||||
"dialogs",
|
||||
],
|
||||
provides=[],
|
||||
)
|
||||
@@ -1,521 +0,0 @@
|
||||
#include "rolljam_cc1101_ext.h"
|
||||
#include <furi_hal_gpio.h>
|
||||
#include <furi_hal_resources.h>
|
||||
#include <furi_hal_cortex.h>
|
||||
#include <furi_hal_power.h>
|
||||
|
||||
// ============================================================
|
||||
// 5V OTG power
|
||||
// ============================================================
|
||||
|
||||
static bool otg_was_enabled = false;
|
||||
static bool use_flux_capacitor = false;
|
||||
|
||||
void rolljam_ext_set_flux_capacitor(bool enabled) {
|
||||
use_flux_capacitor = enabled;
|
||||
}
|
||||
|
||||
static void rolljam_ext_power_on(void) {
|
||||
otg_was_enabled = furi_hal_power_is_otg_enabled();
|
||||
if(!otg_was_enabled) {
|
||||
uint8_t attempts = 0;
|
||||
while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
|
||||
furi_hal_power_enable_otg();
|
||||
furi_delay_ms(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void rolljam_ext_power_off(void) {
|
||||
if(!otg_was_enabled) {
|
||||
furi_hal_power_disable_otg();
|
||||
}
|
||||
}
|
||||
|
||||
static const GpioPin* pin_mosi = &gpio_ext_pa7;
|
||||
static const GpioPin* pin_miso = &gpio_ext_pa6;
|
||||
static const GpioPin* pin_cs = &gpio_ext_pa4;
|
||||
static const GpioPin* pin_sck = &gpio_ext_pb3;
|
||||
static const GpioPin* pin_gdo0 = &gpio_ext_pb2;
|
||||
static const GpioPin* pin_amp = &gpio_ext_pc3;
|
||||
|
||||
// ============================================================
|
||||
// CC1101 Registers
|
||||
// ============================================================
|
||||
#define CC_IOCFG2 0x00
|
||||
#define CC_IOCFG0 0x02
|
||||
#define CC_FIFOTHR 0x03
|
||||
#define CC_SYNC1 0x04
|
||||
#define CC_SYNC0 0x05
|
||||
#define CC_PKTLEN 0x06
|
||||
#define CC_PKTCTRL1 0x07
|
||||
#define CC_PKTCTRL0 0x08
|
||||
#define CC_FSCTRL1 0x0B
|
||||
#define CC_FSCTRL0 0x0C
|
||||
#define CC_FREQ2 0x0D
|
||||
#define CC_FREQ1 0x0E
|
||||
#define CC_FREQ0 0x0F
|
||||
#define CC_MDMCFG4 0x10
|
||||
#define CC_MDMCFG3 0x11
|
||||
#define CC_MDMCFG2 0x12
|
||||
#define CC_MDMCFG1 0x13
|
||||
#define CC_MDMCFG0 0x14
|
||||
#define CC_DEVIATN 0x15
|
||||
#define CC_MCSM1 0x17
|
||||
#define CC_MCSM0 0x18
|
||||
#define CC_FOCCFG 0x19
|
||||
#define CC_AGCCTRL2 0x1B
|
||||
#define CC_AGCCTRL1 0x1C
|
||||
#define CC_AGCCTRL0 0x1D
|
||||
#define CC_FREND0 0x22
|
||||
#define CC_FSCAL3 0x23
|
||||
#define CC_FSCAL2 0x24
|
||||
#define CC_FSCAL1 0x25
|
||||
#define CC_FSCAL0 0x26
|
||||
#define CC_TEST2 0x2C
|
||||
#define CC_TEST1 0x2D
|
||||
#define CC_TEST0 0x2E
|
||||
#define CC_PATABLE 0x3E
|
||||
#define CC_TXFIFO 0x3F
|
||||
|
||||
#define CC_PARTNUM 0x30
|
||||
#define CC_VERSION 0x31
|
||||
#define CC_MARCSTATE 0x35
|
||||
#define CC_TXBYTES 0x3A
|
||||
|
||||
#define CC_SRES 0x30
|
||||
#define CC_SCAL 0x33
|
||||
#define CC_STX 0x35
|
||||
#define CC_SIDLE 0x36
|
||||
#define CC_SFTX 0x3B
|
||||
|
||||
#define MARC_IDLE 0x01
|
||||
#define MARC_TX 0x13
|
||||
|
||||
// ============================================================
|
||||
// Band calibration
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
uint32_t min_freq;
|
||||
uint32_t max_freq;
|
||||
uint8_t fscal3;
|
||||
uint8_t fscal2;
|
||||
uint8_t fscal1;
|
||||
uint8_t fscal0;
|
||||
} ExtBandCal;
|
||||
|
||||
static const ExtBandCal ext_band_cals[] = {
|
||||
{ 299000000, 348000000, 0xEA, 0x2A, 0x00, 0x1F },
|
||||
{ 386000000, 464000000, 0xE9, 0x2A, 0x00, 0x1F },
|
||||
{ 778000000, 928000000, 0xEA, 0x2A, 0x00, 0x11 },
|
||||
};
|
||||
#define EXT_BAND_CAL_COUNT (sizeof(ext_band_cals) / sizeof(ext_band_cals[0]))
|
||||
|
||||
static const ExtBandCal* ext_get_band_cal(uint32_t freq) {
|
||||
for(size_t i = 0; i < EXT_BAND_CAL_COUNT; i++) {
|
||||
if(freq >= ext_band_cals[i].min_freq && freq <= ext_band_cals[i].max_freq)
|
||||
return &ext_band_cals[i];
|
||||
}
|
||||
return &ext_band_cals[1];
|
||||
}
|
||||
|
||||
static inline void spi_delay(void) {
|
||||
for(int i = 0; i < 16; i++) __NOP();
|
||||
}
|
||||
|
||||
static inline void cs_lo(void) { furi_hal_gpio_write(pin_cs, false); spi_delay(); }
|
||||
static inline void cs_hi(void) { spi_delay(); furi_hal_gpio_write(pin_cs, true); spi_delay(); }
|
||||
|
||||
static bool wait_miso(uint32_t us) {
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
uint32_t s = DWT->CYCCNT;
|
||||
uint32_t t = (SystemCoreClock / 1000000) * us;
|
||||
while(furi_hal_gpio_read(pin_miso)) {
|
||||
if((DWT->CYCCNT - s) > t) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t spi_byte(uint8_t tx) {
|
||||
uint8_t rx = 0;
|
||||
for(int8_t i = 7; i >= 0; i--) {
|
||||
furi_hal_gpio_write(pin_mosi, (tx >> i) & 0x01);
|
||||
spi_delay();
|
||||
furi_hal_gpio_write(pin_sck, true);
|
||||
spi_delay();
|
||||
if(furi_hal_gpio_read(pin_miso)) rx |= (1 << i);
|
||||
furi_hal_gpio_write(pin_sck, false);
|
||||
spi_delay();
|
||||
}
|
||||
return rx;
|
||||
}
|
||||
|
||||
static uint8_t cc_strobe(uint8_t cmd) {
|
||||
cs_lo();
|
||||
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
|
||||
uint8_t s = spi_byte(cmd);
|
||||
cs_hi();
|
||||
return s;
|
||||
}
|
||||
|
||||
static void cc_write(uint8_t a, uint8_t v) {
|
||||
cs_lo();
|
||||
if(!wait_miso(5000)) { cs_hi(); return; }
|
||||
spi_byte(a); spi_byte(v);
|
||||
cs_hi();
|
||||
}
|
||||
|
||||
static uint8_t cc_read_status(uint8_t a) {
|
||||
cs_lo();
|
||||
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
|
||||
spi_byte(a | 0xC0);
|
||||
uint8_t v = spi_byte(0x00);
|
||||
cs_hi();
|
||||
return v;
|
||||
}
|
||||
|
||||
static void cc_write_burst(uint8_t a, const uint8_t* d, uint8_t n) {
|
||||
cs_lo();
|
||||
if(!wait_miso(5000)) { cs_hi(); return; }
|
||||
spi_byte(a | 0x40);
|
||||
for(uint8_t i = 0; i < n; i++) spi_byte(d[i]);
|
||||
cs_hi();
|
||||
}
|
||||
|
||||
static bool cc_reset(void) {
|
||||
cs_hi(); furi_delay_us(30);
|
||||
cs_lo(); furi_delay_us(30);
|
||||
cs_hi(); furi_delay_us(50);
|
||||
cs_lo();
|
||||
if(!wait_miso(10000)) { cs_hi(); return false; }
|
||||
spi_byte(CC_SRES);
|
||||
if(!wait_miso(100000)) { cs_hi(); return false; }
|
||||
cs_hi();
|
||||
furi_delay_ms(5);
|
||||
FURI_LOG_I(TAG, "EXT: Reset OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cc_check(void) {
|
||||
uint8_t p = cc_read_status(CC_PARTNUM);
|
||||
uint8_t v = cc_read_status(CC_VERSION);
|
||||
FURI_LOG_I(TAG, "EXT: PART=0x%02X VER=0x%02X", p, v);
|
||||
return (v == 0x14 || v == 0x04 || v == 0x03);
|
||||
}
|
||||
|
||||
static uint8_t cc_state(void) { return cc_read_status(CC_MARCSTATE) & 0x1F; }
|
||||
static uint8_t cc_txbytes(void) { return cc_read_status(CC_TXBYTES) & 0x7F; }
|
||||
|
||||
static void cc_idle(void) {
|
||||
cc_strobe(CC_SIDLE);
|
||||
for(int i = 0; i < 500; i++) {
|
||||
if(cc_state() == MARC_IDLE) return;
|
||||
furi_delay_us(50);
|
||||
}
|
||||
}
|
||||
|
||||
static void cc_set_freq(uint32_t f) {
|
||||
uint32_t r = (uint32_t)(((uint64_t)f << 16) / 26000000ULL);
|
||||
cc_write(CC_FREQ2, (r >> 16) & 0xFF);
|
||||
cc_write(CC_FREQ1, (r >> 8) & 0xFF);
|
||||
cc_write(CC_FREQ0, r & 0xFF);
|
||||
}
|
||||
|
||||
static bool cc_configure_jam(uint32_t freq) {
|
||||
const ExtBandCal* cal = ext_get_band_cal(freq);
|
||||
FURI_LOG_I(TAG, "EXT: Config OOK jam at %lu Hz", freq);
|
||||
cc_idle();
|
||||
cc_write(CC_IOCFG0, 0x02);
|
||||
cc_write(CC_IOCFG2, 0x2F);
|
||||
cc_write(CC_PKTCTRL0, 0x00);
|
||||
cc_write(CC_PKTCTRL1, 0x00);
|
||||
cc_write(CC_PKTLEN, 0xFF);
|
||||
cc_write(CC_FIFOTHR, 0x07);
|
||||
cc_write(CC_SYNC1, 0x00);
|
||||
cc_write(CC_SYNC0, 0x00);
|
||||
cc_set_freq(freq);
|
||||
cc_write(CC_FSCTRL1, 0x06);
|
||||
cc_write(CC_FSCTRL0, 0x00);
|
||||
cc_write(CC_MDMCFG4, 0x85);
|
||||
cc_write(CC_MDMCFG3, 0x43);
|
||||
cc_write(CC_MDMCFG2, 0x30);
|
||||
cc_write(CC_MDMCFG1, 0x00);
|
||||
cc_write(CC_MDMCFG0, 0xF8);
|
||||
cc_write(CC_DEVIATN, 0x47);
|
||||
cc_write(CC_MCSM1, 0x00);
|
||||
cc_write(CC_MCSM0, 0x18);
|
||||
cc_write(CC_FREND0, 0x11);
|
||||
uint8_t pa[8] = {0x00,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0};
|
||||
cc_write_burst(CC_PATABLE, pa, 8);
|
||||
cc_write(CC_FSCAL3, cal->fscal3);
|
||||
cc_write(CC_FSCAL2, cal->fscal2);
|
||||
cc_write(CC_FSCAL1, cal->fscal1);
|
||||
cc_write(CC_FSCAL0, cal->fscal0);
|
||||
cc_write(CC_TEST2, 0x81);
|
||||
cc_write(CC_TEST1, 0x35);
|
||||
cc_write(CC_TEST0, 0x09);
|
||||
cc_idle();
|
||||
cc_strobe(CC_SCAL);
|
||||
furi_delay_ms(2);
|
||||
cc_idle();
|
||||
uint8_t st = cc_state();
|
||||
FURI_LOG_I(TAG, "EXT: state=0x%02X FSCAL={0x%02X,0x%02X,0x%02X,0x%02X}",
|
||||
st, cal->fscal3, cal->fscal2, cal->fscal1, cal->fscal0);
|
||||
return (st == MARC_IDLE);
|
||||
}
|
||||
|
||||
static bool cc_configure_jam_fsk(uint32_t freq, bool wide) {
|
||||
const ExtBandCal* cal = ext_get_band_cal(freq);
|
||||
FURI_LOG_I(TAG, "EXT: Config FSK jam at %lu Hz (wide=%d)", freq, wide);
|
||||
cc_idle();
|
||||
cc_write(CC_IOCFG0, 0x02);
|
||||
cc_write(CC_IOCFG2, 0x2F);
|
||||
cc_write(CC_PKTCTRL0, 0x00);
|
||||
cc_write(CC_PKTCTRL1, 0x00);
|
||||
cc_write(CC_PKTLEN, 0xFF);
|
||||
cc_write(CC_FIFOTHR, 0x07);
|
||||
cc_write(CC_SYNC1, 0x00);
|
||||
cc_write(CC_SYNC0, 0x00);
|
||||
cc_set_freq(freq);
|
||||
cc_write(CC_FSCTRL1, 0x06);
|
||||
cc_write(CC_FSCTRL0, 0x00);
|
||||
cc_write(CC_MDMCFG4, 0x85);
|
||||
cc_write(CC_MDMCFG3, 0x43);
|
||||
cc_write(CC_MDMCFG2, 0x00);
|
||||
cc_write(CC_MDMCFG1, 0x00);
|
||||
cc_write(CC_MDMCFG0, 0xF8);
|
||||
cc_write(CC_DEVIATN, wide ? 0x47 : 0x15);
|
||||
cc_write(CC_MCSM1, 0x00);
|
||||
cc_write(CC_MCSM0, 0x18);
|
||||
cc_write(CC_FREND0, 0x10);
|
||||
uint8_t pa[8] = {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0};
|
||||
cc_write_burst(CC_PATABLE, pa, 8);
|
||||
cc_write(CC_FSCAL3, cal->fscal3);
|
||||
cc_write(CC_FSCAL2, cal->fscal2);
|
||||
cc_write(CC_FSCAL1, cal->fscal1);
|
||||
cc_write(CC_FSCAL0, cal->fscal0);
|
||||
cc_write(CC_TEST2, 0x81);
|
||||
cc_write(CC_TEST1, 0x35);
|
||||
cc_write(CC_TEST0, 0x09);
|
||||
cc_idle();
|
||||
cc_strobe(CC_SCAL);
|
||||
furi_delay_ms(2);
|
||||
cc_idle();
|
||||
return (cc_state() == MARC_IDLE);
|
||||
}
|
||||
|
||||
static void ext_gpio_init_spi_pins(void) {
|
||||
furi_hal_gpio_init(pin_cs, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
|
||||
furi_hal_gpio_write(pin_cs, true);
|
||||
furi_hal_gpio_init(pin_sck, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
|
||||
furi_hal_gpio_write(pin_sck, false);
|
||||
furi_hal_gpio_init(pin_mosi, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
|
||||
furi_hal_gpio_write(pin_mosi, false);
|
||||
furi_hal_gpio_init(pin_miso, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh);
|
||||
furi_hal_gpio_init(pin_gdo0, GpioModeInput, GpioPullDown, GpioSpeedVeryHigh);
|
||||
}
|
||||
|
||||
static void ext_gpio_deinit_spi_pins(void) {
|
||||
furi_hal_gpio_init(pin_cs, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
furi_hal_gpio_init(pin_sck, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
furi_hal_gpio_init(pin_mosi, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
furi_hal_gpio_init(pin_miso, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
furi_hal_gpio_init(pin_gdo0, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
}
|
||||
|
||||
void rolljam_ext_gpio_init(void) {
|
||||
FURI_LOG_I(TAG, "EXT GPIO init (deferred to jam thread)");
|
||||
if(use_flux_capacitor) {
|
||||
furi_hal_gpio_init_simple(pin_amp, GpioModeOutputPushPull);
|
||||
furi_hal_gpio_write(pin_amp, false);
|
||||
}
|
||||
}
|
||||
|
||||
void rolljam_ext_gpio_deinit(void) {
|
||||
if(use_flux_capacitor) {
|
||||
furi_hal_gpio_write(pin_amp, false);
|
||||
furi_hal_gpio_init_simple(pin_amp, GpioModeAnalog);
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "EXT GPIO deinit");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Noise pattern & jam helpers
|
||||
// ============================================================
|
||||
|
||||
static void jam_start_tx(const uint8_t* pattern, uint8_t len) {
|
||||
cc_strobe(CC_SFTX);
|
||||
furi_delay_ms(1);
|
||||
cc_write_burst(CC_TXFIFO, pattern, len);
|
||||
cc_strobe(CC_STX);
|
||||
furi_delay_ms(5);
|
||||
}
|
||||
|
||||
static int32_t jam_thread_worker(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
bool is_fsk = (app->mod_index == ModIndex_FM238 || app->mod_index == ModIndex_FM476);
|
||||
uint32_t freq_pos = app->frequency + app->jam_offset_hz;
|
||||
uint32_t freq_neg = app->frequency - app->jam_offset_hz;
|
||||
|
||||
FURI_LOG_I(TAG, "JAM thread start: target=%lu offset=%lu FSK=%d",
|
||||
app->frequency, app->jam_offset_hz, is_fsk);
|
||||
|
||||
ext_gpio_init_spi_pins();
|
||||
furi_delay_ms(5);
|
||||
|
||||
if(!cc_reset()) {
|
||||
FURI_LOG_E(TAG, "JAM: Reset failed — CC1101 externo no conectado o mal cableado");
|
||||
ext_gpio_deinit_spi_pins();
|
||||
app->jamming_active = false;
|
||||
return -1;
|
||||
}
|
||||
if(!cc_check()) {
|
||||
FURI_LOG_E(TAG, "JAM: Chip no detectado");
|
||||
ext_gpio_deinit_spi_pins();
|
||||
app->jamming_active = false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool jam_ok;
|
||||
if(app->mod_index == ModIndex_FM238)
|
||||
jam_ok = cc_configure_jam_fsk(freq_pos, false);
|
||||
else if(app->mod_index == ModIndex_FM476)
|
||||
jam_ok = cc_configure_jam_fsk(freq_pos, true);
|
||||
else
|
||||
jam_ok = cc_configure_jam(freq_pos);
|
||||
|
||||
if(!jam_ok) {
|
||||
FURI_LOG_E(TAG, "JAM: Config failed");
|
||||
ext_gpio_deinit_spi_pins();
|
||||
app->jamming_active = false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static const uint8_t noise_pattern[62] = {
|
||||
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
|
||||
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
|
||||
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
|
||||
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
|
||||
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
|
||||
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
|
||||
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
|
||||
0xAA,0x55
|
||||
};
|
||||
|
||||
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, true);
|
||||
jam_start_tx(noise_pattern, 62);
|
||||
|
||||
uint8_t st = cc_state();
|
||||
if(st != MARC_TX) {
|
||||
cc_idle();
|
||||
jam_start_tx(noise_pattern, 62);
|
||||
st = cc_state();
|
||||
if(st != MARC_TX) {
|
||||
FURI_LOG_E(TAG, "JAM: Cannot enter TX (state=0x%02X)", st);
|
||||
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, false);
|
||||
ext_gpio_deinit_spi_pins();
|
||||
app->jamming_active = false;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "JAM: *** ACTIVE *** freq_pos=%lu", freq_pos);
|
||||
|
||||
uint32_t loops = 0;
|
||||
uint32_t underflows = 0;
|
||||
uint32_t refills = 0;
|
||||
bool on_pos = true;
|
||||
|
||||
while(app->jam_thread_running) {
|
||||
loops++;
|
||||
|
||||
if(is_fsk && (loops % 4 == 0)) {
|
||||
cc_idle();
|
||||
cc_strobe(CC_SFTX);
|
||||
furi_delay_us(100);
|
||||
on_pos = !on_pos;
|
||||
cc_set_freq(on_pos ? freq_pos : freq_neg);
|
||||
cc_write_burst(CC_TXFIFO, noise_pattern, 62);
|
||||
cc_strobe(CC_STX);
|
||||
furi_delay_ms(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
st = cc_state();
|
||||
if(st != MARC_TX) {
|
||||
underflows++;
|
||||
cc_idle();
|
||||
cc_strobe(CC_SFTX);
|
||||
furi_delay_us(100);
|
||||
cc_write_burst(CC_TXFIFO, noise_pattern, 62);
|
||||
cc_strobe(CC_STX);
|
||||
furi_delay_ms(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t txb = cc_txbytes();
|
||||
if(txb < 20) {
|
||||
uint8_t space = 62 - txb;
|
||||
if(space > 50) space = 50;
|
||||
cc_write_burst(CC_TXFIFO, noise_pattern, space);
|
||||
refills++;
|
||||
}
|
||||
|
||||
if(loops % 500 == 0) {
|
||||
FURI_LOG_I(TAG, "JAM: loops=%lu uf=%lu refills=%lu txb=%d",
|
||||
loops, underflows, refills, cc_txbytes());
|
||||
}
|
||||
|
||||
furi_delay_ms(50);
|
||||
}
|
||||
|
||||
cc_idle();
|
||||
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, false);
|
||||
cc_write(CC_IOCFG2, 0x2E);
|
||||
|
||||
ext_gpio_deinit_spi_pins();
|
||||
|
||||
FURI_LOG_I(TAG, "JAM: STOPPED (loops=%lu uf=%lu refills=%lu)", loops, underflows, refills);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Public API
|
||||
// ============================================================
|
||||
|
||||
void rolljam_jammer_start(RollJamApp* app) {
|
||||
if(app->jamming_active) return;
|
||||
|
||||
app->jam_frequency = app->frequency + app->jam_offset_hz;
|
||||
app->jam_thread_running = true;
|
||||
app->jamming_active = true;
|
||||
|
||||
rolljam_ext_power_on();
|
||||
furi_delay_ms(50);
|
||||
|
||||
rolljam_ext_gpio_init();
|
||||
|
||||
app->jam_thread = furi_thread_alloc_ex("RJ_Jam", 4096, jam_thread_worker, app);
|
||||
furi_thread_start(app->jam_thread);
|
||||
|
||||
FURI_LOG_I(TAG, ">>> JAMMER THREAD STARTED <<<");
|
||||
}
|
||||
|
||||
void rolljam_jammer_stop(RollJamApp* app) {
|
||||
if(!app->jamming_active) return;
|
||||
|
||||
app->jam_thread_running = false;
|
||||
furi_thread_join(app->jam_thread);
|
||||
furi_thread_free(app->jam_thread);
|
||||
app->jam_thread = NULL;
|
||||
|
||||
rolljam_ext_gpio_deinit();
|
||||
rolljam_ext_power_off();
|
||||
app->jamming_active = false;
|
||||
|
||||
FURI_LOG_I(TAG, ">>> JAMMER STOPPED <<<");
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../rolljam.h"
|
||||
|
||||
/*
|
||||
* External CC1101 module connected via GPIO (bit-bang SPI).
|
||||
* Used EXCLUSIVELY for JAMMING (TX).
|
||||
*
|
||||
* Wiring (as connected):
|
||||
* CC1101 VCC -> Flipper Pin 9 (3V3)
|
||||
* CC1101 GND -> Flipper Pin 11 (GND)
|
||||
* CC1101 MOSI -> Flipper Pin 2 (PA7)
|
||||
* CC1101 MISO -> Flipper Pin 3 (PA6)
|
||||
* CC1101 SCK -> Flipper Pin 5 (PB3)
|
||||
* CC1101 CS -> Flipper Pin 4 (PA4)
|
||||
* CC1101 GDO0 -> Flipper Pin 6 (PB2)
|
||||
*/
|
||||
|
||||
void rolljam_ext_gpio_init(void);
|
||||
void rolljam_ext_set_flux_capacitor(bool enabled);
|
||||
void rolljam_ext_gpio_deinit(void);
|
||||
void rolljam_jammer_start(RollJamApp* app);
|
||||
void rolljam_jammer_stop(RollJamApp* app);
|
||||
@@ -1,689 +0,0 @@
|
||||
#include "rolljam_receiver.h"
|
||||
#include <furi_hal_subghz.h>
|
||||
#include <furi_hal_rtc.h>
|
||||
|
||||
#define CC_IOCFG0 0x02
|
||||
#define CC_FIFOTHR 0x03
|
||||
#define CC_MDMCFG4 0x10
|
||||
#define CC_MDMCFG3 0x11
|
||||
#define CC_MDMCFG2 0x12
|
||||
#define CC_MDMCFG1 0x13
|
||||
#define CC_MDMCFG0 0x14
|
||||
#define CC_DEVIATN 0x15
|
||||
#define CC_MCSM0 0x18
|
||||
#define CC_FOCCFG 0x19
|
||||
#define CC_AGCCTRL2 0x1B
|
||||
#define CC_AGCCTRL1 0x1C
|
||||
#define CC_AGCCTRL0 0x1D
|
||||
#define CC_FREND0 0x22
|
||||
#define CC_FSCAL3 0x23
|
||||
#define CC_FSCAL2 0x24
|
||||
#define CC_FSCAL1 0x25
|
||||
#define CC_FSCAL0 0x26
|
||||
|
||||
#define CC_PKTCTRL0 0x08
|
||||
#define CC_PKTCTRL1 0x07
|
||||
#define CC_FSCTRL1 0x0B
|
||||
#define CC_WORCTRL 0x20
|
||||
#define CC_FREND1 0x21
|
||||
|
||||
// OOK 650kHz
|
||||
static const uint8_t preset_ook_650_async[] = {
|
||||
CC_IOCFG0, 0x0D,
|
||||
CC_FIFOTHR, 0x07,
|
||||
CC_PKTCTRL0, 0x32,
|
||||
CC_FSCTRL1, 0x06,
|
||||
CC_MDMCFG0, 0x00,
|
||||
CC_MDMCFG1, 0x00,
|
||||
CC_MDMCFG2, 0x30,
|
||||
CC_MDMCFG3, 0x32,
|
||||
CC_MDMCFG4, 0x17,
|
||||
CC_MCSM0, 0x18,
|
||||
CC_FOCCFG, 0x18,
|
||||
CC_AGCCTRL0, 0x91,
|
||||
CC_AGCCTRL1, 0x00,
|
||||
CC_AGCCTRL2, 0x07,
|
||||
CC_WORCTRL, 0xFB,
|
||||
CC_FREND0, 0x11,
|
||||
CC_FREND1, 0xB6,
|
||||
0x00, 0x00,
|
||||
0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// OOK 270kHz
|
||||
static const uint8_t preset_ook_270_async[] = {
|
||||
CC_IOCFG0, 0x0D,
|
||||
CC_FIFOTHR, 0x47,
|
||||
CC_PKTCTRL0, 0x32,
|
||||
CC_FSCTRL1, 0x06,
|
||||
CC_MDMCFG0, 0x00,
|
||||
CC_MDMCFG1, 0x00,
|
||||
CC_MDMCFG2, 0x30,
|
||||
CC_MDMCFG3, 0x32,
|
||||
CC_MDMCFG4, 0x67,
|
||||
CC_MCSM0, 0x18,
|
||||
CC_FOCCFG, 0x18,
|
||||
CC_AGCCTRL0, 0x40,
|
||||
CC_AGCCTRL1, 0x00,
|
||||
CC_AGCCTRL2, 0x03,
|
||||
CC_WORCTRL, 0xFB,
|
||||
CC_FREND0, 0x11,
|
||||
CC_FREND1, 0xB6,
|
||||
0x00, 0x00,
|
||||
0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// 2FSK Dev 2.38kHz
|
||||
static const uint8_t preset_2fsk_238_async[] = {
|
||||
CC_IOCFG0, 0x0D,
|
||||
CC_FIFOTHR, 0x47,
|
||||
CC_PKTCTRL0, 0x32,
|
||||
CC_FSCTRL1, 0x06,
|
||||
CC_MDMCFG0, 0x00,
|
||||
CC_MDMCFG1, 0x00,
|
||||
CC_MDMCFG2, 0x00,
|
||||
CC_MDMCFG3, 0x75,
|
||||
CC_MDMCFG4, 0x57,
|
||||
CC_DEVIATN, 0x15,
|
||||
CC_MCSM0, 0x18,
|
||||
CC_FOCCFG, 0x16,
|
||||
CC_AGCCTRL0, 0x91,
|
||||
CC_AGCCTRL1, 0x00,
|
||||
CC_AGCCTRL2, 0x07,
|
||||
CC_WORCTRL, 0xFB,
|
||||
CC_FREND0, 0x10,
|
||||
CC_FREND1, 0xB6,
|
||||
0x00, 0x00,
|
||||
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// 2FSK Dev 47.6kHz
|
||||
static const uint8_t preset_2fsk_476_async[] = {
|
||||
CC_IOCFG0, 0x0D,
|
||||
CC_FIFOTHR, 0x47,
|
||||
CC_PKTCTRL0, 0x32,
|
||||
CC_FSCTRL1, 0x06,
|
||||
CC_MDMCFG0, 0x00,
|
||||
CC_MDMCFG1, 0x00,
|
||||
CC_MDMCFG2, 0x00,
|
||||
CC_MDMCFG3, 0x75,
|
||||
CC_MDMCFG4, 0x57,
|
||||
CC_DEVIATN, 0x47,
|
||||
CC_MCSM0, 0x18,
|
||||
CC_FOCCFG, 0x16,
|
||||
CC_AGCCTRL0, 0x91,
|
||||
CC_AGCCTRL1, 0x00,
|
||||
CC_AGCCTRL2, 0x07,
|
||||
CC_WORCTRL, 0xFB,
|
||||
CC_FREND0, 0x10,
|
||||
CC_FREND1, 0xB6,
|
||||
0x00, 0x00,
|
||||
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// TX OOK
|
||||
static const uint8_t preset_ook_tx[] = {
|
||||
CC_IOCFG0, 0x0D,
|
||||
CC_FIFOTHR, 0x07,
|
||||
CC_PKTCTRL0, 0x32,
|
||||
CC_FSCTRL1, 0x06,
|
||||
CC_MDMCFG0, 0x00,
|
||||
CC_MDMCFG1, 0x00,
|
||||
CC_MDMCFG2, 0x30,
|
||||
CC_MDMCFG3, 0x32,
|
||||
CC_MDMCFG4, 0x17,
|
||||
CC_MCSM0, 0x18,
|
||||
CC_FOCCFG, 0x18,
|
||||
CC_AGCCTRL0, 0x91,
|
||||
CC_AGCCTRL1, 0x00,
|
||||
CC_AGCCTRL2, 0x07,
|
||||
CC_WORCTRL, 0xFB,
|
||||
CC_FREND0, 0x11,
|
||||
CC_FREND1, 0xB6,
|
||||
0x00, 0x00,
|
||||
0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static const uint8_t preset_fsk_tx_238[] = {
|
||||
CC_IOCFG0, 0x0D,
|
||||
CC_FIFOTHR, 0x47,
|
||||
CC_PKTCTRL0, 0x32,
|
||||
CC_FSCTRL1, 0x06,
|
||||
CC_MDMCFG0, 0x00,
|
||||
CC_MDMCFG1, 0x00,
|
||||
CC_MDMCFG2, 0x00,
|
||||
CC_MDMCFG3, 0x75,
|
||||
CC_MDMCFG4, 0x57,
|
||||
CC_DEVIATN, 0x15,
|
||||
CC_MCSM0, 0x18,
|
||||
CC_FOCCFG, 0x16,
|
||||
CC_AGCCTRL0, 0x91,
|
||||
CC_AGCCTRL1, 0x00,
|
||||
CC_AGCCTRL2, 0x07,
|
||||
CC_WORCTRL, 0xFB,
|
||||
CC_FREND0, 0x10,
|
||||
CC_FREND1, 0xB6,
|
||||
0x00, 0x00,
|
||||
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static const uint8_t preset_fsk_tx_476[] = {
|
||||
CC_IOCFG0, 0x0D,
|
||||
CC_FIFOTHR, 0x47,
|
||||
CC_PKTCTRL0, 0x32,
|
||||
CC_FSCTRL1, 0x06,
|
||||
CC_MDMCFG0, 0x00,
|
||||
CC_MDMCFG1, 0x00,
|
||||
CC_MDMCFG2, 0x00,
|
||||
CC_MDMCFG3, 0x75,
|
||||
CC_MDMCFG4, 0x57,
|
||||
CC_DEVIATN, 0x47,
|
||||
CC_MCSM0, 0x18,
|
||||
CC_FOCCFG, 0x16,
|
||||
CC_AGCCTRL0, 0x91,
|
||||
CC_AGCCTRL1, 0x00,
|
||||
CC_AGCCTRL2, 0x07,
|
||||
CC_WORCTRL, 0xFB,
|
||||
CC_FREND0, 0x10,
|
||||
CC_FREND1, 0xB6,
|
||||
0x00, 0x00,
|
||||
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Capture state machine
|
||||
// ============================================================
|
||||
|
||||
#define MIN_PULSE_US 100
|
||||
#define MAX_PULSE_US 32767
|
||||
#define SILENCE_GAP_US 50000
|
||||
#define MIN_FRAME_PULSES 40
|
||||
#define AUTO_ACCEPT_PULSES 300
|
||||
#define MAX_CONTINUOUS_SAMPLES 800
|
||||
|
||||
static bool rolljam_is_jammer_pattern_mod(RawSignal* s, uint8_t mod_index) {
|
||||
if(s->size < 20) return false;
|
||||
|
||||
// Calcular estadísticas una sola vez
|
||||
int16_t max_abs = 0;
|
||||
int64_t sum = 0;
|
||||
for(size_t i = 0; i < s->size; i++) {
|
||||
int16_t v = s->data[i] > 0 ? s->data[i] : -s->data[i];
|
||||
if(v > max_abs) max_abs = v;
|
||||
sum += v;
|
||||
}
|
||||
int32_t mean = (int32_t)(sum / (int64_t)s->size);
|
||||
|
||||
FURI_LOG_D(TAG, "JamCheck: mod=%d max=%d mean=%ld size=%d",
|
||||
mod_index, max_abs, mean, (int)s->size);
|
||||
|
||||
if(mod_index == 2 || mod_index == 3) { // ModIndex_FM238=2, FM476=3
|
||||
if((int)s->size < 120) {
|
||||
FURI_LOG_W(TAG, "Jammer FSK rechazado: size=%d < 120", (int)s->size);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if(max_abs < 25000) {
|
||||
FURI_LOG_W(TAG, "Jammer AM650 rechazado: max=%d < 25000", max_abs);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(mod_index == 1) { // ModIndex_AM270=1
|
||||
if(mean < 3000) {
|
||||
FURI_LOG_W(TAG, "Jammer AM270 rechazado: mean=%ld < 3000 (max=%d)", mean, max_abs);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#define MIN_VARIANCE 2000
|
||||
|
||||
static bool rolljam_has_sufficient_variance(RawSignal* s) {
|
||||
if(s->size < 20) return false;
|
||||
|
||||
int64_t sum = 0;
|
||||
for(size_t i = 0; i < s->size; i++) {
|
||||
int16_t val = s->data[i];
|
||||
sum += (val > 0) ? val : -val;
|
||||
}
|
||||
int32_t mean = (int32_t)(sum / (int64_t)s->size);
|
||||
|
||||
int64_t var_sum = 0;
|
||||
for(size_t i = 0; i < s->size; i++) {
|
||||
int16_t val = s->data[i];
|
||||
int32_t abs_val = (val > 0) ? val : -val;
|
||||
int32_t diff = abs_val - mean;
|
||||
var_sum += (int64_t)diff * diff;
|
||||
}
|
||||
int32_t variance = (int32_t)(var_sum / (int64_t)s->size);
|
||||
|
||||
bool has_var = (variance > MIN_VARIANCE);
|
||||
FURI_LOG_I(TAG, "Variance: mean=%ld var=%ld %s",
|
||||
mean, variance, has_var ? "PASS" : "FAIL");
|
||||
return has_var;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
CapWaiting,
|
||||
CapRecording,
|
||||
CapDone,
|
||||
} CapState;
|
||||
|
||||
typedef struct {
|
||||
volatile CapState state;
|
||||
volatile int valid_count;
|
||||
volatile int total_count;
|
||||
volatile bool target_first;
|
||||
volatile uint32_t callback_count;
|
||||
volatile uint32_t continuous_count;
|
||||
float rssi_baseline;
|
||||
uint8_t mod_index;
|
||||
} CapCtx;
|
||||
|
||||
static CapCtx g_cap;
|
||||
|
||||
static void cap_ctx_reset(CapCtx* c) {
|
||||
c->state = CapWaiting;
|
||||
c->valid_count = 0;
|
||||
c->total_count = 0;
|
||||
c->callback_count = 0;
|
||||
c->continuous_count = 0;
|
||||
}
|
||||
|
||||
static void capture_rx_callback(bool level, uint32_t duration, void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
if(!app->raw_capture_active) return;
|
||||
if(g_cap.state == CapDone) return;
|
||||
|
||||
g_cap.callback_count++;
|
||||
|
||||
RawSignal* target = g_cap.target_first ? &app->signal_first : &app->signal_second;
|
||||
if(target->valid) return;
|
||||
|
||||
uint32_t dur = duration;
|
||||
bool is_silence = (dur > SILENCE_GAP_US);
|
||||
bool is_medium_gap = (dur > 5000 && dur <= SILENCE_GAP_US);
|
||||
if(dur > 32767) dur = 32767;
|
||||
|
||||
switch(g_cap.state) {
|
||||
case CapWaiting:
|
||||
g_cap.continuous_count = 0;
|
||||
if(dur >= MIN_PULSE_US && dur <= MAX_PULSE_US && !is_silence) {
|
||||
target->size = 0;
|
||||
g_cap.valid_count = 0;
|
||||
g_cap.total_count = 0;
|
||||
g_cap.state = CapRecording;
|
||||
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
|
||||
target->data[target->size++] = s;
|
||||
g_cap.valid_count++;
|
||||
g_cap.total_count++;
|
||||
g_cap.continuous_count = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case CapRecording:
|
||||
g_cap.continuous_count++;
|
||||
|
||||
if(g_cap.continuous_count > MAX_CONTINUOUS_SAMPLES && !is_medium_gap && !is_silence) {
|
||||
target->size = 0;
|
||||
cap_ctx_reset(&g_cap);
|
||||
return;
|
||||
}
|
||||
|
||||
if(target->size >= RAW_SIGNAL_MAX_SIZE) {
|
||||
g_cap.state = (g_cap.valid_count >= MIN_FRAME_PULSES) ? CapDone : CapWaiting;
|
||||
if(g_cap.state == CapWaiting) {
|
||||
target->size = 0;
|
||||
g_cap.valid_count = 0;
|
||||
g_cap.total_count = 0;
|
||||
g_cap.continuous_count = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_silence) {
|
||||
if(g_cap.valid_count >= MIN_FRAME_PULSES) {
|
||||
if(target->size < RAW_SIGNAL_MAX_SIZE)
|
||||
target->data[target->size++] = level ? (int16_t)32767 : -32767;
|
||||
g_cap.state = CapDone;
|
||||
} else {
|
||||
target->size = 0;
|
||||
cap_ctx_reset(&g_cap);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_medium_gap) g_cap.continuous_count = 0;
|
||||
|
||||
{
|
||||
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
|
||||
target->data[target->size++] = s;
|
||||
g_cap.total_count++;
|
||||
if(dur >= MIN_PULSE_US && dur <= MAX_PULSE_US) {
|
||||
g_cap.valid_count++;
|
||||
if(g_cap.valid_count >= AUTO_ACCEPT_PULSES)
|
||||
g_cap.state = CapDone;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CapDone:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Capture start/stop
|
||||
// ============================================================
|
||||
|
||||
void rolljam_capture_start(RollJamApp* app) {
|
||||
FURI_LOG_I(TAG, "Capture start: freq=%lu mod=%d offset=%lu",
|
||||
app->frequency, app->mod_index, app->jam_offset_hz);
|
||||
|
||||
const uint8_t* src_preset;
|
||||
switch(app->mod_index) {
|
||||
case ModIndex_AM270: src_preset = preset_ook_270_async; break;
|
||||
case ModIndex_FM238: src_preset = preset_2fsk_238_async; break;
|
||||
case ModIndex_FM476: src_preset = preset_2fsk_476_async; break;
|
||||
default: src_preset = preset_ook_650_async; break;
|
||||
}
|
||||
|
||||
furi_hal_subghz_load_custom_preset(src_preset);
|
||||
furi_delay_ms(5);
|
||||
|
||||
uint32_t real_freq = furi_hal_subghz_set_frequency_and_path(app->frequency);
|
||||
FURI_LOG_I(TAG, "Capture: freq=%lu (requested %lu)", real_freq, app->frequency);
|
||||
furi_delay_ms(5);
|
||||
|
||||
furi_hal_subghz_rx();
|
||||
furi_delay_ms(50);
|
||||
float rssi_baseline = furi_hal_subghz_get_rssi();
|
||||
g_cap.rssi_baseline = rssi_baseline;
|
||||
FURI_LOG_I(TAG, "Capture: RSSI baseline=%.1f dBm", (double)rssi_baseline);
|
||||
|
||||
furi_hal_subghz_idle();
|
||||
furi_delay_ms(5);
|
||||
|
||||
cap_ctx_reset(&g_cap);
|
||||
|
||||
if(!app->signal_first.valid) {
|
||||
g_cap.target_first = true;
|
||||
app->signal_first.size = 0;
|
||||
app->signal_first.valid = false;
|
||||
FURI_LOG_I(TAG, "Capture target: FIRST signal");
|
||||
} else {
|
||||
g_cap.target_first = false;
|
||||
app->signal_second.size = 0;
|
||||
app->signal_second.valid = false;
|
||||
FURI_LOG_I(TAG, "Capture target: SECOND signal");
|
||||
}
|
||||
|
||||
g_cap.mod_index = app->mod_index;
|
||||
app->raw_capture_active = true;
|
||||
furi_hal_subghz_start_async_rx(capture_rx_callback, app);
|
||||
FURI_LOG_I(TAG, "Capture: RX STARTED");
|
||||
}
|
||||
|
||||
void rolljam_capture_stop(RollJamApp* app) {
|
||||
if(!app->raw_capture_active) {
|
||||
FURI_LOG_W(TAG, "Capture stop: was not active");
|
||||
return;
|
||||
}
|
||||
app->raw_capture_active = false;
|
||||
furi_hal_subghz_stop_async_rx();
|
||||
furi_delay_ms(5);
|
||||
FURI_LOG_I(TAG, "Capture stopped. cb=%lu state=%d valid=%d total=%d",
|
||||
g_cap.callback_count, g_cap.state, g_cap.valid_count, g_cap.total_count);
|
||||
FURI_LOG_I(TAG, " Sig1: size=%d valid=%d", app->signal_first.size, app->signal_first.valid);
|
||||
FURI_LOG_I(TAG, " Sig2: size=%d valid=%d", app->signal_second.size, app->signal_second.valid);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Validation
|
||||
// ============================================================
|
||||
|
||||
bool rolljam_signal_is_valid(RawSignal* signal) {
|
||||
if(g_cap.state != CapDone) {
|
||||
static int check_count = 0;
|
||||
check_count++;
|
||||
if(check_count % 10 == 0)
|
||||
FURI_LOG_D(TAG, "Validate: state=%d cb=%lu valid=%d total=%d size=%d",
|
||||
g_cap.state, g_cap.callback_count,
|
||||
g_cap.valid_count, g_cap.total_count, (int)signal->size);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(signal->size < (size_t)MIN_FRAME_PULSES) return false;
|
||||
|
||||
if(rolljam_is_jammer_pattern_mod(signal, g_cap.mod_index)) {
|
||||
signal->size = 0;
|
||||
cap_ctx_reset(&g_cap);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!rolljam_has_sufficient_variance(signal)) {
|
||||
signal->size = 0;
|
||||
cap_ctx_reset(&g_cap);
|
||||
return false;
|
||||
}
|
||||
|
||||
int good = 0;
|
||||
int total = (int)signal->size;
|
||||
for(int i = 0; i < total; i++) {
|
||||
int16_t abs_val = signal->data[i] > 0 ? signal->data[i] : -signal->data[i];
|
||||
if(abs_val >= MIN_PULSE_US) good++;
|
||||
}
|
||||
int ratio_pct = (total > 0) ? ((good * 100) / total) : 0;
|
||||
|
||||
if(ratio_pct > 50 && good >= MIN_FRAME_PULSES) {
|
||||
FURI_LOG_I(TAG, "Signal VALID: %d/%d (%d%%) size=%d", good, total, ratio_pct, total);
|
||||
return true;
|
||||
}
|
||||
|
||||
FURI_LOG_D(TAG, "Signal rejected: %d/%d (%d%%)", good, total, ratio_pct);
|
||||
signal->size = 0;
|
||||
cap_ctx_reset(&g_cap);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Signal cleanup
|
||||
// ============================================================
|
||||
|
||||
void rolljam_signal_cleanup(RawSignal* signal) {
|
||||
if(signal->size < (size_t)MIN_FRAME_PULSES) return;
|
||||
|
||||
int16_t* cleaned = malloc(RAW_SIGNAL_MAX_SIZE * sizeof(int16_t));
|
||||
if(!cleaned) return;
|
||||
size_t out = 0;
|
||||
|
||||
size_t start = 0;
|
||||
while(start < signal->size) {
|
||||
int16_t abs_val = signal->data[start] > 0 ? signal->data[start] : -signal->data[start];
|
||||
if(abs_val >= MIN_PULSE_US) break;
|
||||
start++;
|
||||
}
|
||||
|
||||
for(size_t i = start; i < signal->size; i++) {
|
||||
int16_t val = signal->data[i];
|
||||
int16_t abs_val = val > 0 ? val : -val;
|
||||
bool is_positive = (val > 0);
|
||||
|
||||
if(abs_val < MIN_PULSE_US) {
|
||||
if(out > 0) {
|
||||
int16_t prev = cleaned[out - 1];
|
||||
bool prev_positive = (prev > 0);
|
||||
int16_t prev_abs = prev > 0 ? prev : -prev;
|
||||
if(prev_positive == is_positive) {
|
||||
int32_t merged = (int32_t)prev_abs + abs_val;
|
||||
if(merged > 32767) merged = 32767;
|
||||
cleaned[out - 1] = prev_positive ? (int16_t)merged : -(int16_t)merged;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t q = ((abs_val + 50) / 100) * 100;
|
||||
if(q < MIN_PULSE_US) q = MIN_PULSE_US;
|
||||
if(q > 32767) q = 32767;
|
||||
|
||||
if(out < RAW_SIGNAL_MAX_SIZE)
|
||||
cleaned[out++] = is_positive ? (int16_t)q : -(int16_t)q;
|
||||
}
|
||||
|
||||
while(out > 0) {
|
||||
int16_t abs_last = cleaned[out-1] > 0 ? cleaned[out-1] : -cleaned[out-1];
|
||||
if(abs_last >= MIN_PULSE_US && abs_last < 32767) break;
|
||||
out--;
|
||||
}
|
||||
|
||||
if(out >= (size_t)MIN_FRAME_PULSES) {
|
||||
size_t orig = signal->size;
|
||||
memcpy(signal->data, cleaned, out * sizeof(int16_t));
|
||||
signal->size = out;
|
||||
FURI_LOG_I(TAG, "Cleanup: %d -> %d samples", (int)orig, (int)out);
|
||||
}
|
||||
free(cleaned);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TX
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
const int16_t* data;
|
||||
size_t size;
|
||||
volatile size_t index;
|
||||
} TxCtx;
|
||||
|
||||
static TxCtx g_tx;
|
||||
|
||||
static LevelDuration tx_feed(void* context) {
|
||||
UNUSED(context);
|
||||
if(g_tx.index >= g_tx.size) return level_duration_reset();
|
||||
int16_t sample = g_tx.data[g_tx.index++];
|
||||
bool level = (sample > 0);
|
||||
uint32_t dur = (uint32_t)(sample > 0 ? sample : -sample);
|
||||
return level_duration_make(level, dur);
|
||||
}
|
||||
|
||||
void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal) {
|
||||
if(!signal->valid || signal->size == 0) {
|
||||
FURI_LOG_E(TAG, "TX: no valid signal");
|
||||
return;
|
||||
}
|
||||
FURI_LOG_I(TAG, "TX: %d samples at %lu Hz (3x)", (int)signal->size, app->frequency);
|
||||
|
||||
const uint8_t* tx_src;
|
||||
switch(app->mod_index) {
|
||||
case ModIndex_FM238: tx_src = preset_fsk_tx_238; break;
|
||||
case ModIndex_FM476: tx_src = preset_fsk_tx_476; break;
|
||||
default: tx_src = preset_ook_tx; break;
|
||||
}
|
||||
furi_hal_subghz_load_custom_preset(tx_src);
|
||||
uint32_t real_freq = furi_hal_subghz_set_frequency_and_path(app->frequency);
|
||||
FURI_LOG_I(TAG, "TX: freq=%lu", real_freq);
|
||||
furi_hal_subghz_idle();
|
||||
furi_delay_ms(5);
|
||||
|
||||
for(int tx_repeat = 0; tx_repeat < 3; tx_repeat++) {
|
||||
g_tx.data = signal->data;
|
||||
g_tx.size = signal->size;
|
||||
g_tx.index = 0;
|
||||
|
||||
if(!furi_hal_subghz_start_async_tx(tx_feed, NULL)) {
|
||||
FURI_LOG_E(TAG, "TX: start failed on repeat %d!", tx_repeat);
|
||||
furi_hal_subghz_idle();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t timeout = 0;
|
||||
while(!furi_hal_subghz_is_async_tx_complete()) {
|
||||
furi_delay_ms(5);
|
||||
if(++timeout > 2000) {
|
||||
FURI_LOG_E(TAG, "TX: timeout on repeat %d!", tx_repeat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
furi_hal_subghz_stop_async_tx();
|
||||
FURI_LOG_I(TAG, "TX: repeat %d done (%d/%d)",
|
||||
tx_repeat, (int)g_tx.index, (int)signal->size);
|
||||
if(tx_repeat < 2) furi_delay_ms(50);
|
||||
}
|
||||
furi_hal_subghz_idle();
|
||||
FURI_LOG_I(TAG, "TX: all repeats done");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Save
|
||||
// ============================================================
|
||||
|
||||
void rolljam_save_signal(RollJamApp* app, RawSignal* signal) {
|
||||
if(!signal->valid || signal->size == 0) {
|
||||
FURI_LOG_E(TAG, "Save: no signal");
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime dt;
|
||||
furi_hal_rtc_get_datetime(&dt);
|
||||
|
||||
FuriString* path = furi_string_alloc_printf(
|
||||
"/ext/subghz/RJ_%04d%02d%02d_%02d%02d%02d.sub",
|
||||
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
|
||||
|
||||
FURI_LOG_I(TAG, "Saving: %s", furi_string_get_cstr(path));
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
storage_simply_mkdir(storage, "/ext/subghz");
|
||||
File* file = storage_file_alloc(storage);
|
||||
|
||||
if(storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||
FuriString* line = furi_string_alloc();
|
||||
|
||||
furi_string_set(line, "Filetype: Flipper SubGhz RAW File\n");
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
furi_string_printf(line, "Version: 1\n");
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
furi_string_printf(line, "Frequency: %lu\n", app->frequency);
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
|
||||
const char* pname;
|
||||
switch(app->mod_index) {
|
||||
case ModIndex_AM270: pname = "FuriHalSubGhzPresetOok270Async"; break;
|
||||
case ModIndex_FM238: pname = "FuriHalSubGhzPreset2FSKDev238Async"; break;
|
||||
case ModIndex_FM476: pname = "FuriHalSubGhzPreset2FSKDev476Async"; break;
|
||||
default: pname = "FuriHalSubGhzPresetOok650Async"; break;
|
||||
}
|
||||
furi_string_printf(line, "Preset: %s\n", pname);
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
furi_string_printf(line, "Protocol: RAW\n");
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
|
||||
size_t i = 0;
|
||||
while(i < signal->size) {
|
||||
furi_string_set(line, "RAW_Data:");
|
||||
size_t end = i + 512;
|
||||
if(end > signal->size) end = signal->size;
|
||||
for(; i < end; i++)
|
||||
furi_string_cat_printf(line, " %d", signal->data[i]);
|
||||
furi_string_cat(line, "\n");
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
}
|
||||
furi_string_free(line);
|
||||
FURI_LOG_I(TAG, "Saved: %d samples", (int)signal->size);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Save failed!");
|
||||
}
|
||||
|
||||
storage_file_close(file);
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
furi_string_free(path);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../rolljam.h"
|
||||
|
||||
/*
|
||||
* Internal CC1101 raw signal capture and transmission.
|
||||
*
|
||||
* Capture: uses narrow RX bandwidth so the offset jamming
|
||||
* from the external CC1101 is filtered out.
|
||||
*
|
||||
* The captured raw data is stored as signed int16 values:
|
||||
* positive = high-level duration (microseconds)
|
||||
* negative = low-level duration (microseconds)
|
||||
*
|
||||
* This matches the Flipper .sub RAW format.
|
||||
*/
|
||||
|
||||
void rolljam_capture_start(RollJamApp* app);
|
||||
void rolljam_capture_stop(RollJamApp* app);
|
||||
|
||||
bool rolljam_signal_is_valid(RawSignal* signal);
|
||||
|
||||
void rolljam_signal_cleanup(RawSignal* signal);
|
||||
void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal);
|
||||
void rolljam_save_signal(RollJamApp* app, RawSignal* signal);
|
||||
@@ -1,21 +0,0 @@
|
||||
applications_user/rolljam/
|
||||
├── application.fam
|
||||
├── rolljam.png (icon 10x10)
|
||||
├── rolljam.c
|
||||
├── rolljam_icons.h
|
||||
├── scenes/
|
||||
│ ├── rolljam_scene.h
|
||||
│ ├── rolljam_scene_config.h
|
||||
│ ├── rolljam_scene_menu.c
|
||||
│ ├── rolljam_scene_attack_phase1.c
|
||||
│ ├── rolljam_scene_attack_phase2.c
|
||||
│ ├── rolljam_scene_attack_phase3.c
|
||||
│ └── rolljam_scene_result.c
|
||||
├── helpers/
|
||||
│ ├── rolljam_cc1101_ext.h
|
||||
│ ├── rolljam_cc1101_ext.c
|
||||
│ ├── rolljam_receiver.h
|
||||
│ └── rolljam_receiver.c
|
||||
└── views/
|
||||
├── rolljam_attack_view.h
|
||||
└── rolljam_attack_view.c
|
||||
@@ -1,232 +0,0 @@
|
||||
#include "rolljam.h"
|
||||
#include "scenes/rolljam_scene.h"
|
||||
#include "helpers/rolljam_cc1101_ext.h"
|
||||
#include "helpers/rolljam_receiver.h"
|
||||
#include "helpers/rolljam_cc1101_ext.h"
|
||||
|
||||
// ============================================================
|
||||
// Frequency / modulation tables
|
||||
// ============================================================
|
||||
|
||||
const uint32_t freq_values[] = {
|
||||
300000000,
|
||||
303875000,
|
||||
315000000,
|
||||
318000000,
|
||||
390000000,
|
||||
433075000,
|
||||
433920000,
|
||||
434420000,
|
||||
438900000,
|
||||
868350000,
|
||||
915000000,
|
||||
};
|
||||
|
||||
const char* freq_names[] = {
|
||||
"300.00",
|
||||
"303.87",
|
||||
"315.00",
|
||||
"318.00",
|
||||
"390.00",
|
||||
"433.07",
|
||||
"433.92",
|
||||
"434.42",
|
||||
"438.90",
|
||||
"868.35",
|
||||
"915.00",
|
||||
};
|
||||
|
||||
const char* mod_names[] = {
|
||||
"AM 650",
|
||||
"AM 270",
|
||||
"FM 238",
|
||||
"FM 476",
|
||||
};
|
||||
|
||||
const uint32_t jam_offset_values[] = {
|
||||
300000,
|
||||
500000,
|
||||
700000,
|
||||
1000000,
|
||||
};
|
||||
|
||||
const char* jam_offset_names[] = {
|
||||
"300 kHz",
|
||||
"500 kHz",
|
||||
"700 kHz",
|
||||
"1000 kHz",
|
||||
};
|
||||
|
||||
const char* hw_names[] = {
|
||||
"CC1101",
|
||||
"Flux Cap",
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Scene handlers table (extern declarations in scene header)
|
||||
// ============================================================
|
||||
|
||||
void (*const rolljam_scene_on_enter_handlers[])(void*) = {
|
||||
rolljam_scene_menu_on_enter,
|
||||
rolljam_scene_attack_phase1_on_enter,
|
||||
rolljam_scene_attack_phase2_on_enter,
|
||||
rolljam_scene_attack_phase3_on_enter,
|
||||
rolljam_scene_result_on_enter,
|
||||
};
|
||||
|
||||
bool (*const rolljam_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
|
||||
rolljam_scene_menu_on_event,
|
||||
rolljam_scene_attack_phase1_on_event,
|
||||
rolljam_scene_attack_phase2_on_event,
|
||||
rolljam_scene_attack_phase3_on_event,
|
||||
rolljam_scene_result_on_event,
|
||||
};
|
||||
|
||||
void (*const rolljam_scene_on_exit_handlers[])(void*) = {
|
||||
rolljam_scene_menu_on_exit,
|
||||
rolljam_scene_attack_phase1_on_exit,
|
||||
rolljam_scene_attack_phase2_on_exit,
|
||||
rolljam_scene_attack_phase3_on_exit,
|
||||
rolljam_scene_result_on_exit,
|
||||
};
|
||||
|
||||
const SceneManagerHandlers rolljam_scene_handlers = {
|
||||
.on_enter_handlers = rolljam_scene_on_enter_handlers,
|
||||
.on_event_handlers = rolljam_scene_on_event_handlers,
|
||||
.on_exit_handlers = rolljam_scene_on_exit_handlers,
|
||||
.scene_num = RollJamSceneCount,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Navigation callbacks
|
||||
// ============================================================
|
||||
|
||||
static bool rolljam_navigation_callback(void* context) {
|
||||
RollJamApp* app = context;
|
||||
return scene_manager_handle_back_event(app->scene_manager);
|
||||
}
|
||||
|
||||
static bool rolljam_custom_event_callback(void* context, uint32_t event) {
|
||||
RollJamApp* app = context;
|
||||
return scene_manager_handle_custom_event(app->scene_manager, event);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// App alloc
|
||||
// ============================================================
|
||||
|
||||
static RollJamApp* rolljam_app_alloc(void) {
|
||||
RollJamApp* app = malloc(sizeof(RollJamApp));
|
||||
memset(app, 0, sizeof(RollJamApp));
|
||||
|
||||
app->freq_index = FreqIndex_433_92;
|
||||
app->frequency = freq_values[FreqIndex_433_92];
|
||||
app->mod_index = ModIndex_AM650;
|
||||
app->jam_offset_index = JamOffIndex_700k;
|
||||
app->jam_offset_hz = jam_offset_values[JamOffIndex_700k];
|
||||
app->hw_index = HwIndex_CC1101;
|
||||
|
||||
// Services
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
app->notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
app->storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
// Scene manager
|
||||
app->scene_manager = scene_manager_alloc(&rolljam_scene_handlers, app);
|
||||
|
||||
// View dispatcher
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||
view_dispatcher_set_custom_event_callback(
|
||||
app->view_dispatcher, rolljam_custom_event_callback);
|
||||
view_dispatcher_set_navigation_event_callback(
|
||||
app->view_dispatcher, rolljam_navigation_callback);
|
||||
view_dispatcher_attach_to_gui(
|
||||
app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||
|
||||
// Variable item list
|
||||
app->var_item_list = variable_item_list_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher,
|
||||
RollJamViewVarItemList,
|
||||
variable_item_list_get_view(app->var_item_list));
|
||||
|
||||
// Widget
|
||||
app->widget = widget_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher,
|
||||
RollJamViewWidget,
|
||||
widget_get_view(app->widget));
|
||||
|
||||
// Dialog
|
||||
app->dialog_ex = dialog_ex_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher,
|
||||
RollJamViewDialogEx,
|
||||
dialog_ex_get_view(app->dialog_ex));
|
||||
|
||||
// Popup
|
||||
app->popup = popup_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher,
|
||||
RollJamViewPopup,
|
||||
popup_get_view(app->popup));
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// App free
|
||||
// ============================================================
|
||||
|
||||
static void rolljam_app_free(RollJamApp* app) {
|
||||
if(app->jamming_active) {
|
||||
rolljam_jammer_stop(app);
|
||||
}
|
||||
if(app->raw_capture_active) {
|
||||
rolljam_capture_stop(app);
|
||||
}
|
||||
|
||||
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewVarItemList);
|
||||
variable_item_list_free(app->var_item_list);
|
||||
|
||||
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewWidget);
|
||||
widget_free(app->widget);
|
||||
|
||||
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewDialogEx);
|
||||
dialog_ex_free(app->dialog_ex);
|
||||
|
||||
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewPopup);
|
||||
popup_free(app->popup);
|
||||
|
||||
scene_manager_free(app->scene_manager);
|
||||
view_dispatcher_free(app->view_dispatcher);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
free(app);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Entry point
|
||||
// ============================================================
|
||||
|
||||
int32_t rolljam_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
RollJamApp* app = rolljam_app_alloc();
|
||||
|
||||
FURI_LOG_I(TAG, "=== RollJam Started ===");
|
||||
FURI_LOG_I(TAG, "Internal CC1101 = RX capture (narrow BW)");
|
||||
FURI_LOG_I(TAG, "External CC1101 = TX jam (offset +%lu Hz)", app->jam_offset_hz);
|
||||
|
||||
scene_manager_next_scene(app->scene_manager, RollJamSceneMenu);
|
||||
view_dispatcher_run(app->view_dispatcher);
|
||||
|
||||
rolljam_app_free(app);
|
||||
|
||||
FURI_LOG_I(TAG, "=== RollJam Stopped ===");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/scene_manager.h>
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/modules/popup.h>
|
||||
#include <gui/modules/variable_item_list.h>
|
||||
#include <gui/modules/widget.h>
|
||||
#include <gui/modules/dialog_ex.h>
|
||||
#include <notification/notification.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include <storage/storage.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TAG "RollJam"
|
||||
|
||||
#define RAW_SIGNAL_MAX_SIZE 4096
|
||||
|
||||
// ============================================================
|
||||
// Frequencies
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
FreqIndex_300_00 = 0,
|
||||
FreqIndex_303_87,
|
||||
FreqIndex_315_00,
|
||||
FreqIndex_318_00,
|
||||
FreqIndex_390_00,
|
||||
FreqIndex_433_07,
|
||||
FreqIndex_433_92,
|
||||
FreqIndex_434_42,
|
||||
FreqIndex_438_90,
|
||||
FreqIndex_868_35,
|
||||
FreqIndex_915_00,
|
||||
FreqIndex_COUNT,
|
||||
} FreqIndex;
|
||||
|
||||
extern const uint32_t freq_values[];
|
||||
extern const char* freq_names[];
|
||||
|
||||
// ============================================================
|
||||
// Modulations
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
ModIndex_AM650 = 0,
|
||||
ModIndex_AM270,
|
||||
ModIndex_FM238,
|
||||
ModIndex_FM476,
|
||||
ModIndex_COUNT,
|
||||
} ModIndex;
|
||||
|
||||
extern const char* mod_names[];
|
||||
|
||||
// ============================================================
|
||||
// Jam offsets
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
JamOffIndex_300k = 0,
|
||||
JamOffIndex_500k,
|
||||
JamOffIndex_700k,
|
||||
JamOffIndex_1000k,
|
||||
JamOffIndex_COUNT,
|
||||
} JamOffIndex;
|
||||
|
||||
extern const uint32_t jam_offset_values[];
|
||||
extern const char* jam_offset_names[];
|
||||
|
||||
// ============================================================
|
||||
// Hardware type
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
HwIndex_CC1101 = 0,
|
||||
HwIndex_FluxCapacitor,
|
||||
HwIndex_COUNT,
|
||||
} HwIndex;
|
||||
|
||||
extern const char* hw_names[];
|
||||
|
||||
// ============================================================
|
||||
// Scenes
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
RollJamSceneMenu,
|
||||
RollJamSceneAttackPhase1,
|
||||
RollJamSceneAttackPhase2,
|
||||
RollJamSceneAttackPhase3,
|
||||
RollJamSceneResult,
|
||||
RollJamSceneCount,
|
||||
} RollJamScene;
|
||||
|
||||
// ============================================================
|
||||
// Views
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
RollJamViewVarItemList,
|
||||
RollJamViewWidget,
|
||||
RollJamViewDialogEx,
|
||||
RollJamViewPopup,
|
||||
} RollJamView;
|
||||
|
||||
// ============================================================
|
||||
// Custom events
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
RollJamEventStartAttack = 100,
|
||||
RollJamEventSignalCaptured,
|
||||
RollJamEventPhase3Done,
|
||||
RollJamEventReplayNow,
|
||||
RollJamEventSaveSignal,
|
||||
RollJamEventBack,
|
||||
} RollJamEvent;
|
||||
|
||||
// ============================================================
|
||||
// Raw signal container
|
||||
// ============================================================
|
||||
typedef struct {
|
||||
int16_t data[RAW_SIGNAL_MAX_SIZE];
|
||||
size_t size;
|
||||
bool valid;
|
||||
} RawSignal;
|
||||
|
||||
// ============================================================
|
||||
// Main app struct
|
||||
// ============================================================
|
||||
typedef struct {
|
||||
Gui* gui;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
SceneManager* scene_manager;
|
||||
NotificationApp* notification;
|
||||
Storage* storage;
|
||||
|
||||
VariableItemList* var_item_list;
|
||||
Widget* widget;
|
||||
DialogEx* dialog_ex;
|
||||
Popup* popup;
|
||||
|
||||
FreqIndex freq_index;
|
||||
ModIndex mod_index;
|
||||
JamOffIndex jam_offset_index;
|
||||
HwIndex hw_index;
|
||||
uint32_t frequency;
|
||||
uint32_t jam_frequency;
|
||||
uint32_t jam_offset_hz;
|
||||
|
||||
RawSignal signal_first;
|
||||
RawSignal signal_second;
|
||||
|
||||
bool jamming_active;
|
||||
FuriThread* jam_thread;
|
||||
volatile bool jam_thread_running;
|
||||
|
||||
volatile bool raw_capture_active;
|
||||
|
||||
|
||||
} RollJamApp;
|
||||
|
Before Width: | Height: | Size: 220 B |
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Icon assets are auto-generated by the build system
|
||||
// from the images/ folder. If no custom icons are needed,
|
||||
// this file can remain minimal.
|
||||
|
||||
// If you place .png files in an images/ folder,
|
||||
// the build system generates icon references automatically.
|
||||
// Access them via &I_iconname
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../rolljam.h"
|
||||
|
||||
// Scene on_enter
|
||||
void rolljam_scene_menu_on_enter(void* context);
|
||||
void rolljam_scene_attack_phase1_on_enter(void* context);
|
||||
void rolljam_scene_attack_phase2_on_enter(void* context);
|
||||
void rolljam_scene_attack_phase3_on_enter(void* context);
|
||||
void rolljam_scene_result_on_enter(void* context);
|
||||
|
||||
// Scene on_event
|
||||
bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event);
|
||||
bool rolljam_scene_attack_phase1_on_event(void* context, SceneManagerEvent event);
|
||||
bool rolljam_scene_attack_phase2_on_event(void* context, SceneManagerEvent event);
|
||||
bool rolljam_scene_attack_phase3_on_event(void* context, SceneManagerEvent event);
|
||||
bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event);
|
||||
|
||||
// Scene on_exit
|
||||
void rolljam_scene_menu_on_exit(void* context);
|
||||
void rolljam_scene_attack_phase1_on_exit(void* context);
|
||||
void rolljam_scene_attack_phase2_on_exit(void* context);
|
||||
void rolljam_scene_attack_phase3_on_exit(void* context);
|
||||
void rolljam_scene_result_on_exit(void* context);
|
||||
|
||||
// Scene manager handlers (defined in rolljam.c)
|
||||
extern const SceneManagerHandlers rolljam_scene_handlers;
|
||||
@@ -1,126 +0,0 @@
|
||||
#include "rolljam_scene.h"
|
||||
#include "../helpers/rolljam_cc1101_ext.h"
|
||||
#include "../helpers/rolljam_receiver.h"
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: JAM + CAPTURE first keyfob press
|
||||
// ============================================================
|
||||
|
||||
static void phase1_timer_callback(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
if(app->signal_first.size >= 20 &&
|
||||
rolljam_signal_is_valid(&app->signal_first)) {
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, RollJamEventSignalCaptured);
|
||||
}
|
||||
}
|
||||
|
||||
void rolljam_scene_attack_phase1_on_enter(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
widget_reset(app->widget);
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 2, AlignCenter, AlignTop,
|
||||
FontPrimary, "PHASE 1 / 4");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 16, AlignCenter, AlignTop,
|
||||
FontSecondary, "Starting...");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 56, AlignCenter, AlignTop,
|
||||
FontSecondary, "[BACK] cancel");
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, RollJamViewWidget);
|
||||
|
||||
rolljam_ext_set_flux_capacitor(app->hw_index == HwIndex_FluxCapacitor);
|
||||
|
||||
rolljam_jammer_start(app);
|
||||
furi_delay_ms(300);
|
||||
|
||||
widget_reset(app->widget);
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 2, AlignCenter, AlignTop,
|
||||
FontPrimary, "PHASE 1 / 4");
|
||||
if(app->jamming_active) {
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 16, AlignCenter, AlignTop,
|
||||
FontSecondary, "Jamming active...");
|
||||
FURI_LOG_I(TAG, "Phase1: jammer activo en %lu Hz", app->jam_frequency);
|
||||
} else {
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 16, AlignCenter, AlignTop,
|
||||
FontSecondary, "No ext jammer");
|
||||
FURI_LOG_W(TAG, "Phase1: sin jammer, capturando de todas formas");
|
||||
}
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 28, AlignCenter, AlignTop,
|
||||
FontSecondary, "Listening for keyfob");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 42, AlignCenter, AlignTop,
|
||||
FontPrimary, "PRESS KEYFOB NOW");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 56, AlignCenter, AlignTop,
|
||||
FontSecondary, "[BACK] cancel");
|
||||
|
||||
rolljam_capture_start(app);
|
||||
|
||||
notification_message(app->notification, &sequence_blink_blue_100);
|
||||
|
||||
FuriTimer* timer = furi_timer_alloc(
|
||||
phase1_timer_callback, FuriTimerTypePeriodic, app);
|
||||
furi_timer_start(timer, 300);
|
||||
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, RollJamSceneAttackPhase1, (uint32_t)timer);
|
||||
|
||||
FURI_LOG_I(TAG, "Phase1: waiting for 1st keyfob press...");
|
||||
}
|
||||
|
||||
bool rolljam_scene_attack_phase1_on_event(void* context, SceneManagerEvent event) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == RollJamEventSignalCaptured) {
|
||||
rolljam_capture_stop(app);
|
||||
|
||||
if(!rolljam_signal_is_valid(&app->signal_first)) {
|
||||
FURI_LOG_W(TAG, "Phase1: false capture, restarting RX...");
|
||||
app->signal_first.size = 0;
|
||||
app->signal_first.valid = false;
|
||||
furi_delay_ms(50);
|
||||
rolljam_capture_start(app);
|
||||
return true;
|
||||
}
|
||||
|
||||
rolljam_signal_cleanup(&app->signal_first);
|
||||
app->signal_first.valid = true;
|
||||
|
||||
notification_message(app->notification, &sequence_success);
|
||||
FURI_LOG_I(TAG, "Phase1: 1st signal captured! size=%d",
|
||||
(int)app->signal_first.size);
|
||||
|
||||
scene_manager_next_scene(app->scene_manager, RollJamSceneAttackPhase2);
|
||||
return true;
|
||||
}
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
FURI_LOG_I(TAG, "Phase1: cancelled");
|
||||
rolljam_capture_stop(app);
|
||||
rolljam_jammer_stop(app);
|
||||
scene_manager_search_and_switch_to_another_scene(
|
||||
app->scene_manager, RollJamSceneMenu);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void rolljam_scene_attack_phase1_on_exit(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
FuriTimer* timer = (FuriTimer*)scene_manager_get_scene_state(
|
||||
app->scene_manager, RollJamSceneAttackPhase1);
|
||||
if(timer) {
|
||||
furi_timer_stop(timer);
|
||||
furi_timer_free(timer);
|
||||
}
|
||||
|
||||
widget_reset(app->widget);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
#include "rolljam_scene.h"
|
||||
#include "../helpers/rolljam_cc1101_ext.h"
|
||||
#include "../helpers/rolljam_receiver.h"
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: JAM + CAPTURE second keyfob press
|
||||
// ============================================================
|
||||
|
||||
static void phase2_timer_callback(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
if(app->signal_second.size >= 20 &&
|
||||
rolljam_signal_is_valid(&app->signal_second)) {
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, RollJamEventSignalCaptured);
|
||||
}
|
||||
}
|
||||
|
||||
void rolljam_scene_attack_phase2_on_enter(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
widget_reset(app->widget);
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 2, AlignCenter, AlignTop,
|
||||
FontPrimary, "PHASE 2 / 4");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 16, AlignCenter, AlignTop,
|
||||
FontSecondary, "1st code CAPTURED!");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 28, AlignCenter, AlignTop,
|
||||
FontSecondary, "Still jamming...");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 42, AlignCenter, AlignTop,
|
||||
FontPrimary, "PRESS KEYFOB AGAIN");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 56, AlignCenter, AlignTop,
|
||||
FontSecondary, "[BACK] cancel");
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, RollJamViewWidget);
|
||||
|
||||
memset(app->signal_second.data, 0, sizeof(app->signal_second.data));
|
||||
app->signal_second.size = 0;
|
||||
app->signal_second.valid = false;
|
||||
|
||||
rolljam_capture_stop(app);
|
||||
furi_delay_ms(50);
|
||||
rolljam_capture_start(app);
|
||||
|
||||
notification_message(app->notification, &sequence_blink_yellow_100);
|
||||
|
||||
FuriTimer* timer = furi_timer_alloc(
|
||||
phase2_timer_callback, FuriTimerTypePeriodic, app);
|
||||
furi_timer_start(timer, 300);
|
||||
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, RollJamSceneAttackPhase2, (uint32_t)timer);
|
||||
|
||||
FURI_LOG_I(TAG, "Phase2: waiting for 2nd keyfob press...");
|
||||
}
|
||||
|
||||
bool rolljam_scene_attack_phase2_on_event(void* context, SceneManagerEvent event) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == RollJamEventSignalCaptured) {
|
||||
rolljam_capture_stop(app);
|
||||
|
||||
if(!rolljam_signal_is_valid(&app->signal_second)) {
|
||||
FURI_LOG_W(TAG, "Phase2: false capture, restarting RX...");
|
||||
app->signal_second.size = 0;
|
||||
app->signal_second.valid = false;
|
||||
furi_delay_ms(50);
|
||||
rolljam_capture_start(app);
|
||||
return true;
|
||||
}
|
||||
|
||||
rolljam_signal_cleanup(&app->signal_second);
|
||||
app->signal_second.valid = true;
|
||||
|
||||
notification_message(app->notification, &sequence_success);
|
||||
FURI_LOG_I(TAG, "Phase2: 2nd signal captured! size=%d",
|
||||
(int)app->signal_second.size);
|
||||
|
||||
rolljam_capture_stop(app);
|
||||
scene_manager_next_scene(app->scene_manager, RollJamSceneAttackPhase3);
|
||||
return true;
|
||||
}
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
FURI_LOG_I(TAG, "Phase2: cancelled");
|
||||
rolljam_capture_stop(app);
|
||||
rolljam_jammer_stop(app);
|
||||
scene_manager_search_and_switch_to_another_scene(
|
||||
app->scene_manager, RollJamSceneMenu);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void rolljam_scene_attack_phase2_on_exit(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
FuriTimer* timer = (FuriTimer*)scene_manager_get_scene_state(
|
||||
app->scene_manager, RollJamSceneAttackPhase2);
|
||||
if(timer) {
|
||||
furi_timer_stop(timer);
|
||||
furi_timer_free(timer);
|
||||
}
|
||||
|
||||
widget_reset(app->widget);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
#include "rolljam_scene.h"
|
||||
#include "../helpers/rolljam_cc1101_ext.h"
|
||||
#include "../helpers/rolljam_receiver.h"
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: STOP jam + REPLAY first signal
|
||||
// The victim device opens. We keep the 2nd (newer) code.
|
||||
// ============================================================
|
||||
|
||||
void rolljam_scene_attack_phase3_on_enter(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
widget_reset(app->widget);
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 2, AlignCenter, AlignTop,
|
||||
FontPrimary, "PHASE 3 / 4");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 18, AlignCenter, AlignTop,
|
||||
FontSecondary, "Stopping jammer...");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 32, AlignCenter, AlignTop,
|
||||
FontPrimary, "REPLAYING 1st CODE");
|
||||
widget_add_string_element(
|
||||
app->widget, 64, 48, AlignCenter, AlignTop,
|
||||
FontSecondary, "Target should open!");
|
||||
|
||||
view_dispatcher_switch_to_view(
|
||||
app->view_dispatcher, RollJamViewWidget);
|
||||
|
||||
notification_message(app->notification, &sequence_blink_green_100);
|
||||
|
||||
rolljam_jammer_stop(app);
|
||||
|
||||
furi_delay_ms(1000);
|
||||
|
||||
rolljam_transmit_signal(app, &app->signal_first);
|
||||
|
||||
FURI_LOG_I(TAG, "Phase3: 1st code replayed. Keeping 2nd code.");
|
||||
|
||||
notification_message(app->notification, &sequence_success);
|
||||
|
||||
furi_delay_ms(800);
|
||||
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, RollJamEventPhase3Done);
|
||||
}
|
||||
|
||||
bool rolljam_scene_attack_phase3_on_event(void* context, SceneManagerEvent event) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == RollJamEventPhase3Done) {
|
||||
scene_manager_next_scene(
|
||||
app->scene_manager, RollJamSceneResult);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void rolljam_scene_attack_phase3_on_exit(void* context) {
|
||||
RollJamApp* app = context;
|
||||
widget_reset(app->widget);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* Scene configuration file.
|
||||
* Lists all scenes for the SceneManager.
|
||||
*
|
||||
* In some Flipper apps this uses ADD_SCENE macros.
|
||||
* We handle it manually via the handlers arrays in rolljam.c
|
||||
* so this file just documents the scene list.
|
||||
*
|
||||
* Scenes:
|
||||
* 0 - RollJamSceneMenu
|
||||
* 1 - RollJamSceneAttackPhase1
|
||||
* 2 - RollJamSceneAttackPhase2
|
||||
* 3 - RollJamSceneAttackPhase3
|
||||
* 4 - RollJamSceneResult
|
||||
*/
|
||||
@@ -1,161 +0,0 @@
|
||||
#include "rolljam_scene.h"
|
||||
|
||||
// ============================================================
|
||||
// Menu scene: select frequency, modulation, start attack
|
||||
// ============================================================
|
||||
|
||||
static uint8_t get_min_offset_index(uint8_t mod_index) {
|
||||
if(mod_index == ModIndex_AM270) return JamOffIndex_1000k;
|
||||
return JamOffIndex_300k;
|
||||
}
|
||||
|
||||
static void enforce_min_offset(RollJamApp* app, VariableItem* offset_item) {
|
||||
uint8_t min_idx = get_min_offset_index(app->mod_index);
|
||||
if(app->jam_offset_index < min_idx) {
|
||||
app->jam_offset_index = min_idx;
|
||||
app->jam_offset_hz = jam_offset_values[min_idx];
|
||||
if(offset_item) {
|
||||
variable_item_set_current_value_index(offset_item, min_idx);
|
||||
variable_item_set_current_value_text(offset_item, jam_offset_names[min_idx]);
|
||||
}
|
||||
FURI_LOG_I(TAG, "Menu: offset ajustado a %s para AM270",
|
||||
jam_offset_names[min_idx]);
|
||||
}
|
||||
}
|
||||
|
||||
static VariableItem* s_offset_item = NULL;
|
||||
|
||||
static void menu_freq_changed(VariableItem* item) {
|
||||
RollJamApp* app = variable_item_get_context(item);
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
app->freq_index = index;
|
||||
app->frequency = freq_values[index];
|
||||
variable_item_set_current_value_text(item, freq_names[index]);
|
||||
}
|
||||
|
||||
static void menu_mod_changed(VariableItem* item) {
|
||||
RollJamApp* app = variable_item_get_context(item);
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
app->mod_index = index;
|
||||
variable_item_set_current_value_text(item, mod_names[index]);
|
||||
|
||||
enforce_min_offset(app, s_offset_item);
|
||||
}
|
||||
|
||||
static void menu_jam_offset_changed(VariableItem* item) {
|
||||
RollJamApp* app = variable_item_get_context(item);
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
|
||||
uint8_t min_idx = get_min_offset_index(app->mod_index);
|
||||
if(index < min_idx) {
|
||||
index = min_idx;
|
||||
variable_item_set_current_value_index(item, index);
|
||||
}
|
||||
|
||||
app->jam_offset_index = index;
|
||||
app->jam_offset_hz = jam_offset_values[index];
|
||||
variable_item_set_current_value_text(item, jam_offset_names[index]);
|
||||
}
|
||||
|
||||
static void menu_hw_changed(VariableItem* item) {
|
||||
RollJamApp* app = variable_item_get_context(item);
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
app->hw_index = index;
|
||||
variable_item_set_current_value_text(item, hw_names[index]);
|
||||
}
|
||||
|
||||
static void menu_enter_callback(void* context, uint32_t index) {
|
||||
RollJamApp* app = context;
|
||||
if(index == 4) {
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, RollJamEventStartAttack);
|
||||
}
|
||||
}
|
||||
|
||||
void rolljam_scene_menu_on_enter(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
variable_item_list_reset(app->var_item_list);
|
||||
|
||||
// --- Frequency ---
|
||||
VariableItem* freq_item = variable_item_list_add(
|
||||
app->var_item_list,
|
||||
"Frequency",
|
||||
FreqIndex_COUNT,
|
||||
menu_freq_changed,
|
||||
app);
|
||||
variable_item_set_current_value_index(freq_item, app->freq_index);
|
||||
variable_item_set_current_value_text(freq_item, freq_names[app->freq_index]);
|
||||
|
||||
// --- Modulation ---
|
||||
VariableItem* mod_item = variable_item_list_add(
|
||||
app->var_item_list,
|
||||
"Modulation",
|
||||
ModIndex_COUNT,
|
||||
menu_mod_changed,
|
||||
app);
|
||||
variable_item_set_current_value_index(mod_item, app->mod_index);
|
||||
variable_item_set_current_value_text(mod_item, mod_names[app->mod_index]);
|
||||
|
||||
// --- Jam Offset ---
|
||||
VariableItem* offset_item = variable_item_list_add(
|
||||
app->var_item_list,
|
||||
"Jam Offset",
|
||||
JamOffIndex_COUNT,
|
||||
menu_jam_offset_changed,
|
||||
app);
|
||||
|
||||
s_offset_item = offset_item;
|
||||
enforce_min_offset(app, offset_item);
|
||||
|
||||
variable_item_set_current_value_index(offset_item, app->jam_offset_index);
|
||||
variable_item_set_current_value_text(offset_item, jam_offset_names[app->jam_offset_index]);
|
||||
|
||||
// --- Hardware ---
|
||||
VariableItem* hw_item = variable_item_list_add(
|
||||
app->var_item_list,
|
||||
"Hardware",
|
||||
HwIndex_COUNT,
|
||||
menu_hw_changed,
|
||||
app);
|
||||
variable_item_set_current_value_index(hw_item, app->hw_index);
|
||||
variable_item_set_current_value_text(hw_item, hw_names[app->hw_index]);
|
||||
|
||||
// --- Start button ---
|
||||
variable_item_list_add(
|
||||
app->var_item_list,
|
||||
">> START ATTACK <<",
|
||||
0,
|
||||
NULL,
|
||||
app);
|
||||
|
||||
variable_item_list_set_enter_callback(
|
||||
app->var_item_list, menu_enter_callback, app);
|
||||
|
||||
view_dispatcher_switch_to_view(
|
||||
app->view_dispatcher, RollJamViewVarItemList);
|
||||
}
|
||||
|
||||
bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == RollJamEventStartAttack) {
|
||||
enforce_min_offset(app, NULL);
|
||||
|
||||
memset(&app->signal_first, 0, sizeof(RawSignal));
|
||||
memset(&app->signal_second, 0, sizeof(RawSignal));
|
||||
|
||||
scene_manager_next_scene(
|
||||
app->scene_manager, RollJamSceneAttackPhase1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void rolljam_scene_menu_on_exit(void* context) {
|
||||
RollJamApp* app = context;
|
||||
s_offset_item = NULL;
|
||||
variable_item_list_reset(app->var_item_list);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
#include "rolljam_scene.h"
|
||||
#include "../helpers/rolljam_receiver.h"
|
||||
|
||||
// ============================================================
|
||||
// Phase 4 / Result: user chooses to SAVE or REPLAY 2nd code
|
||||
// ============================================================
|
||||
|
||||
static void result_dialog_callback(DialogExResult result, void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
if(result == DialogExResultLeft) {
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, RollJamEventSaveSignal);
|
||||
} else if(result == DialogExResultRight) {
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, RollJamEventReplayNow);
|
||||
}
|
||||
}
|
||||
|
||||
void rolljam_scene_result_on_enter(void* context) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
dialog_ex_reset(app->dialog_ex);
|
||||
|
||||
dialog_ex_set_header(
|
||||
app->dialog_ex, "Attack Complete!",
|
||||
64, 2, AlignCenter, AlignTop);
|
||||
|
||||
dialog_ex_set_text(
|
||||
app->dialog_ex,
|
||||
"1st code: SENT to target\n"
|
||||
"2nd code: IN MEMORY\n\n"
|
||||
"What to do with 2nd?",
|
||||
64, 18, AlignCenter, AlignTop);
|
||||
|
||||
dialog_ex_set_left_button_text(app->dialog_ex, "Save");
|
||||
dialog_ex_set_right_button_text(app->dialog_ex, "Send");
|
||||
|
||||
dialog_ex_set_result_callback(app->dialog_ex, result_dialog_callback);
|
||||
dialog_ex_set_context(app->dialog_ex, app);
|
||||
|
||||
view_dispatcher_switch_to_view(
|
||||
app->view_dispatcher, RollJamViewDialogEx);
|
||||
}
|
||||
|
||||
bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event) {
|
||||
RollJamApp* app = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == RollJamEventSaveSignal) {
|
||||
|
||||
rolljam_save_signal(app, &app->signal_second);
|
||||
|
||||
popup_reset(app->popup);
|
||||
popup_set_header(
|
||||
app->popup, "Saved!",
|
||||
64, 20, AlignCenter, AlignCenter);
|
||||
popup_set_text(
|
||||
app->popup,
|
||||
"File saved to:\n/ext/subghz/rolljam_*.sub\n\nPress Back",
|
||||
64, 38, AlignCenter, AlignCenter);
|
||||
popup_set_timeout(app->popup, 5000);
|
||||
popup_enable_timeout(app->popup);
|
||||
view_dispatcher_switch_to_view(
|
||||
app->view_dispatcher, RollJamViewPopup);
|
||||
|
||||
notification_message(app->notification, &sequence_success);
|
||||
return true;
|
||||
|
||||
} else if(event.event == RollJamEventReplayNow) {
|
||||
|
||||
popup_reset(app->popup);
|
||||
popup_set_header(
|
||||
app->popup, "Transmitting...",
|
||||
64, 20, AlignCenter, AlignCenter);
|
||||
popup_set_text(
|
||||
app->popup, "Sending 2nd code NOW",
|
||||
64, 38, AlignCenter, AlignCenter);
|
||||
view_dispatcher_switch_to_view(
|
||||
app->view_dispatcher, RollJamViewPopup);
|
||||
|
||||
rolljam_transmit_signal(app, &app->signal_second);
|
||||
|
||||
notification_message(app->notification, &sequence_success);
|
||||
|
||||
popup_set_header(
|
||||
app->popup, "Done!",
|
||||
64, 20, AlignCenter, AlignCenter);
|
||||
popup_set_text(
|
||||
app->popup,
|
||||
"2nd code transmitted!\n\nPress Back",
|
||||
64, 38, AlignCenter, AlignCenter);
|
||||
popup_set_timeout(app->popup, 5000);
|
||||
popup_enable_timeout(app->popup);
|
||||
|
||||
return true;
|
||||
}
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
scene_manager_search_and_switch_to_another_scene(
|
||||
app->scene_manager, RollJamSceneMenu);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void rolljam_scene_result_on_exit(void* context) {
|
||||
RollJamApp* app = context;
|
||||
dialog_ex_reset(app->dialog_ex);
|
||||
popup_reset(app->popup);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
#include "rolljam_attack_view.h"
|
||||
#include <gui/canvas.h>
|
||||
|
||||
// ============================================================
|
||||
// Custom drawing for attack status
|
||||
// Reserved for future use with a custom View
|
||||
// Currently the app uses Widget modules instead
|
||||
// ============================================================
|
||||
|
||||
void rolljam_attack_view_draw(Canvas* canvas, AttackViewState* state) {
|
||||
canvas_clear(canvas);
|
||||
|
||||
// Title bar
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 64, 2, AlignCenter, AlignTop, state->phase_text);
|
||||
|
||||
// Separator
|
||||
canvas_draw_line(canvas, 0, 14, 128, 14);
|
||||
|
||||
// Status
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 64, 18, AlignCenter, AlignTop, state->status_text);
|
||||
|
||||
// Indicators
|
||||
int y = 32;
|
||||
|
||||
if(state->jamming) {
|
||||
canvas_draw_str(canvas, 4, y, "JAM: [ACTIVE]");
|
||||
// Animated dots could go here
|
||||
} else {
|
||||
canvas_draw_str(canvas, 4, y, "JAM: [OFF]");
|
||||
}
|
||||
y += 12;
|
||||
|
||||
if(state->capturing) {
|
||||
canvas_draw_str(canvas, 4, y, "RX: [LISTENING]");
|
||||
} else {
|
||||
canvas_draw_str(canvas, 4, y, "RX: [OFF]");
|
||||
}
|
||||
y += 12;
|
||||
|
||||
// Signal counter
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "Signals: %d / 2", state->signal_count);
|
||||
canvas_draw_str(canvas, 4, y, buf);
|
||||
|
||||
// Footer
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 64, 62, AlignCenter, AlignBottom, "[BACK] cancel");
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../rolljam.h"
|
||||
|
||||
/*
|
||||
* Custom view for attack visualization.
|
||||
* Currently the app uses Widget and DialogEx for display.
|
||||
* This file is reserved for a future custom canvas-drawn view
|
||||
* (e.g., signal waveform display, animated jamming indicator).
|
||||
*
|
||||
* For now it provides a simple status draw function.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
const char* phase_text;
|
||||
const char* status_text;
|
||||
bool jamming;
|
||||
bool capturing;
|
||||
int signal_count;
|
||||
} AttackViewState;
|
||||
|
||||
// Draw attack status on a canvas (for future custom View use)
|
||||
void rolljam_attack_view_draw(Canvas* canvas, AttackViewState* state);
|
||||
@@ -4,14 +4,12 @@ App(
|
||||
apptype=FlipperAppType.METAPACKAGE,
|
||||
provides=[
|
||||
"gpio",
|
||||
"infrared",
|
||||
"lfrfid",
|
||||
"nfc",
|
||||
"subghz",
|
||||
"rolljam",
|
||||
"subghz_remote",
|
||||
"subghz_bruteforcer",
|
||||
"archive",
|
||||
"subghz_remote",
|
||||
"main_apps_on_start",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "subghz_txrx_i.h" // IWYU pragma: keep
|
||||
|
||||
#include <math.h>
|
||||
#include <furi_hal_subghz.h>
|
||||
#include <lib/subghz/protocols/protocol_items.h>
|
||||
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
|
||||
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
|
||||
#include "../../../../lib/subghz/devices/subghz_preset_delta.h"
|
||||
#include <lib/subghz/blocks/custom_btn.h>
|
||||
|
||||
#define TAG "SubGhzTxRx"
|
||||
@@ -498,6 +500,36 @@ void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
|
||||
}
|
||||
}
|
||||
|
||||
// Identify the hop index (0=AM650, 1=FM476, 2=FM95) from the name.
|
||||
// Must match the order defined in subghz_preset_delta.h
|
||||
static int subghz_hop_index_from_name(const char* name) {
|
||||
if(strcmp(name, "AM650") == 0) return 0;
|
||||
if(strcmp(name, "FM476") == 0) return 1;
|
||||
if(strcmp(name, "FM95") == 0) return 2;
|
||||
return -1; // is not part of the fast hopping set
|
||||
}
|
||||
|
||||
// Applies the target preset using delta-patch (without SRES) when possible,
|
||||
// or falls back to the original full reload in any other case.
|
||||
static void subghz_txrx_apply_preset_fast(
|
||||
SubGhzTxRx* instance,
|
||||
const char* old_preset_name,
|
||||
const char* preset_name) {
|
||||
int from_idx = subghz_hop_index_from_name(old_preset_name);
|
||||
int to_idx = subghz_hop_index_from_name(preset_name);
|
||||
|
||||
if(instance->radio_device_type == SubGhzRadioDeviceTypeInternal && from_idx >= 0 &&
|
||||
to_idx >= 0 && from_idx != to_idx) {
|
||||
// Fast path: delta-patch without SRES or full reload (only internal CC1101)
|
||||
const PresetDeltaEntry* e = &preset_delta_table[from_idx][to_idx];
|
||||
furi_hal_subghz_apply_preset_delta(e->delta, e->delta_len, e->needs_scal, e->pa_table);
|
||||
} else {
|
||||
// Fallback: original behavior (full reload)
|
||||
subghz_devices_load_preset(
|
||||
instance->radio_device, FuriHalSubGhzPresetCustom, instance->preset->data);
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold) {
|
||||
furi_assert(instance);
|
||||
|
||||
@@ -550,22 +582,7 @@ void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold
|
||||
subghz_txrx_set_preset_internal(
|
||||
instance, instance->preset->frequency, actual_preset_idx, 0);
|
||||
|
||||
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
|
||||
bool new_is_am = (strstr(preset_name, "AM") != NULL);
|
||||
bool modulation_changed = (old_is_am != new_is_am);
|
||||
|
||||
if(modulation_changed) {
|
||||
subghz_devices_reset(instance->radio_device);
|
||||
subghz_devices_load_preset(
|
||||
instance->radio_device,
|
||||
FuriHalSubGhzPresetCustom,
|
||||
instance->preset->data);
|
||||
} else {
|
||||
subghz_devices_load_preset(
|
||||
instance->radio_device,
|
||||
FuriHalSubGhzPresetCustom,
|
||||
instance->preset->data);
|
||||
}
|
||||
subghz_txrx_apply_preset_fast(instance, old_preset_name, preset_name);
|
||||
|
||||
subghz_txrx_rx(instance, instance->preset->frequency);
|
||||
}
|
||||
@@ -588,22 +605,7 @@ void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold
|
||||
subghz_txrx_set_preset_internal(
|
||||
instance, instance->preset->frequency, instance->preset_hopper_idx, 0);
|
||||
|
||||
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
|
||||
bool new_is_am = (strstr(preset_name, "AM") != NULL);
|
||||
bool modulation_changed = (old_is_am != new_is_am);
|
||||
|
||||
if(modulation_changed) {
|
||||
subghz_devices_reset(instance->radio_device);
|
||||
subghz_devices_load_preset(
|
||||
instance->radio_device,
|
||||
FuriHalSubGhzPresetCustom,
|
||||
instance->preset->data);
|
||||
} else {
|
||||
subghz_devices_load_preset(
|
||||
instance->radio_device,
|
||||
FuriHalSubGhzPresetCustom,
|
||||
instance->preset->data);
|
||||
}
|
||||
subghz_txrx_apply_preset_fast(instance, old_preset_name, preset_name);
|
||||
|
||||
subghz_txrx_rx(instance, instance->preset->frequency);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Zachary Weiss
|
||||
Copyright (c) 2019 James Howard (original)
|
||||
Copyright (c) 2025 Apfxtech (Flipper Zero port)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -0,0 +1,177 @@
|
||||
# Flipper DOOM
|
||||
|
||||
A DOOM-style first-person shooter demake for the Flipper Zero
|
||||
(128×64 monochrome LCD). Built on the raycaster engine from
|
||||
[FlipperCatacombs](https://github.com/apfxtech/FlipperCatacombs) (a port of
|
||||
*Catacombs of the Damned* / arduboy3d by jhhoward), re-skinned with graphics
|
||||
converted **at build time from your own DOOM shareware WAD**.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Copy `dist/flipdoom.fap` to your SD card under `SD/apps/Games/`
|
||||
(or run `ufbt launch` with the Flipper connected via USB).
|
||||
2. On the Flipper: **Apps → Games → Flipper DOOM**.
|
||||
3. Title screen → main menu → select **Play** with OK.
|
||||
4. Fight through procedurally generated levels, find the exit gate on each
|
||||
floor, survive as deep as you can. Your score and high score are saved
|
||||
automatically.
|
||||
|
||||
---
|
||||
|
||||
## Controls
|
||||
|
||||
### Title screen
|
||||
|
||||
| Button | Action |
|
||||
|---|---|
|
||||
| Any button | Skip the title screen |
|
||||
|
||||
### Main menu
|
||||
|
||||
| Button | Action |
|
||||
|---|---|
|
||||
| **Up / Down** | Move cursor (Play / Sound on-off / Score / High score) |
|
||||
| **OK** | Select item |
|
||||
| **Back (hold ~300 ms)** | Exit the app |
|
||||
|
||||
### In game
|
||||
|
||||
| Button | Action |
|
||||
|---|---|
|
||||
| **Up / Down** | Move forward / backward |
|
||||
| **Left / Right** | Turn left / right |
|
||||
| **OK** | Fire the shotgun (hold for continuous fire) |
|
||||
| **OK held + Left / Right** | **Strafe** (circle-strafe while firing) — essential for dodging fireballs |
|
||||
| **Back (hold ~300 ms)** | Leave the game and return to the menu |
|
||||
|
||||
Notes:
|
||||
|
||||
- Firing costs ammo (bottom HUD bar); ammo regenerates when not shooting.
|
||||
- Strafing only works while OK is held. Tap OK to fire while turning
|
||||
normally; hold OK to switch the side arrows into dodge mode.
|
||||
- Walking into a backpack opens it; walking over pickups collects them.
|
||||
|
||||
### HUD
|
||||
|
||||
- **Bottom bar with cross icon** — health.
|
||||
- **Bar above it with bullets icon** — ammo.
|
||||
- Screen border flashes when you take damage.
|
||||
- Pickup/event messages appear at the top of the screen.
|
||||
|
||||
---
|
||||
|
||||
## Gameplay
|
||||
|
||||
### Enemies
|
||||
|
||||
| Enemy | Behavior |
|
||||
|---|---|
|
||||
| **Zombieman** | Weak, shoots from range, appears from level 1 |
|
||||
| **Sergeant** | Fast, shoots hard, keeps distance |
|
||||
| **Imp** | Throws fireballs, backs away if you get close |
|
||||
| **Demon** | Melee tank — charges you and bites |
|
||||
|
||||
Difficulty scales with depth: early levels spawn mostly zombies; imps join
|
||||
from level 3, and demons dominate from level 6.
|
||||
|
||||
### Exploding barrels
|
||||
|
||||
Shooting a barrel triggers an area explosion that deals heavy damage to every
|
||||
enemy nearby (one-shots zombies and sergeants) and hurts you if you stand too
|
||||
close — though it will never kill you outright, it can leave you at 1 HP.
|
||||
Barrels sometimes leave a pickup behind.
|
||||
|
||||
### Pickups
|
||||
|
||||
| Item | Effect |
|
||||
|---|---|
|
||||
| Stimpack | Restores health |
|
||||
| Backpack (walk into it) | Bonus points |
|
||||
| Armor / helmet / bonus items | Points |
|
||||
|
||||
### Scoring
|
||||
|
||||
Points are awarded for floors cleared, kills per enemy type, and items
|
||||
collected. Escaping the base (final exit) grants a large bonus. Score and
|
||||
high score are stored on the SD card (`apps_data/flipdoom/`) and persist
|
||||
between sessions.
|
||||
|
||||
---
|
||||
|
||||
## Sound
|
||||
|
||||
Tone-based sound effects through the Flipper buzzer (shotgun blast, hits,
|
||||
kills, pickups, player damage), all tuned within the buzzer's physical
|
||||
100–2500 Hz range. Sound can be toggled in the main menu; the system
|
||||
Stealth Mode is respected.
|
||||
|
||||
---
|
||||
|
||||
## Building from source
|
||||
|
||||
Requires Python 3 and [ufbt](https://pypi.org/project/ufbt/):
|
||||
|
||||
```sh
|
||||
pip install ufbt
|
||||
|
||||
cd FlipperDoom
|
||||
|
||||
# 1) Generate the sprite header from YOUR shareware WAD (not included here):
|
||||
python3 tools/extract_doom_assets.py /path/to/Doom1.WAD
|
||||
|
||||
# 2) Build / install:
|
||||
ufbt # produces dist/flipdoom.fap
|
||||
ufbt launch # builds, installs and runs on a connected Flipper
|
||||
```
|
||||
|
||||
### About the assets
|
||||
|
||||
No id Software assets are stored in this repository. The generated header
|
||||
`game/Generated/DoomSprites.inc.h` is produced locally by
|
||||
`tools/extract_doom_assets.py`, which decodes sprites from the user's own
|
||||
shareware WAD (freely distributable as a whole), rescales them and quantizes
|
||||
to 1-bit (black / 50% checker / white) in the engine's sprite formats.
|
||||
Do not redistribute the generated header — always regenerate it from a WAD
|
||||
you own. The "DOOM" title lettering and the HUD icons are original pixel art
|
||||
made for this project. Preview images of the converted assets are written to
|
||||
`tools/preview/`.
|
||||
|
||||
Entity mapping (game mechanics are inherited from the Catacombs engine):
|
||||
|
||||
| Engine entity | DOOM sprite | Role |
|
||||
|---|---|---|
|
||||
| Skeleton | Demon (SARG) | melee tank |
|
||||
| Mage | Imp (TROO) | fireball thrower |
|
||||
| Bat | Sergeant (SPOS) | fast shooter |
|
||||
| Spider | Zombieman (POSS) | weak shooter |
|
||||
| Weapon | Shotgun (SHTG + SHTF flash) | first person, centered |
|
||||
| Urn | Barrel (BAR1) | explodes when shot |
|
||||
| Potion | Stimpack (STIM) | health |
|
||||
| Chest / opened | Backpack (BPAK) / Clip (CLIP) | treasure |
|
||||
| Crown / scroll / coins | Armor / helmet / potion bottle (ARM1, BON2, BON1) | points |
|
||||
| Sign | Skull pile (POL5) | decoration |
|
||||
| Projectiles | Fireball (BAL1) | player & enemies |
|
||||
|
||||
## Why not a real doomgeneric port?
|
||||
|
||||
The hardware makes it impossible — not a software choice:
|
||||
|
||||
| | Flipper Zero | Real DOOM (doomgeneric) |
|
||||
|---|---|---|
|
||||
| Total RAM | 256 KB | — |
|
||||
| Free heap for apps | ~140 KB | ~7 MB (zone memory + framebuffer) |
|
||||
| Engine binary | FAPs load fully into RAM | ~524 KB compiled for Cortex-M4 |
|
||||
|
||||
The Flipper cannot execute code from the SD card (no XIP), so it can't even
|
||||
load the DOOM engine binary. DOOM ports to 256 KB microcontrollers (GBA,
|
||||
nRF52840) rely on memory-mapped flash, which the Flipper does not have. This
|
||||
demake keeps the aesthetic with an engine that actually fits: ~30 KB loaded,
|
||||
30 FPS on the 64 MHz Cortex-M4.
|
||||
|
||||
## License
|
||||
|
||||
Same license as the base project (see `LICENSE`). WAD contents are property
|
||||
of id Software; the extraction tool only transforms them locally for
|
||||
personal use.
|
||||
@@ -0,0 +1,15 @@
|
||||
App(
|
||||
appid="flipdoom",
|
||||
name="Flipper DOOM",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="flipdoom_app",
|
||||
cdefines=["APP_FLIPDOOM"],
|
||||
requires=["gui"],
|
||||
stack_size=8 * 1024,
|
||||
fap_category="Games",
|
||||
fap_icon="flipdoom_icon.png",
|
||||
order=36,
|
||||
fap_author="user",
|
||||
fap_version="1.0",
|
||||
fap_description="Doom-style raycaster demake for Flipper Zero. Graphics converted at build time from your own Doom shareware WAD.",
|
||||
)
|
||||
|
After Width: | Height: | Size: 96 B |
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
static inline const void* pgm_read_ptr_safe(const void* p) {
|
||||
const void* out;
|
||||
std::memcpy(&out, p, sizeof(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Platform detection
|
||||
#if defined(_WIN32)
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#define PROGMEM
|
||||
#define PSTR(s) (s)
|
||||
#define pgm_read_byte(x) (*((uint8_t*)(x)))
|
||||
#define pgm_read_word(x) (*((uint16_t*)(x)))
|
||||
#define pgm_read_ptr(x) (*((uintptr_t*)(x)))
|
||||
#define strlen_P(x) strlen(x)
|
||||
#define strcpy_P(dst, src) strcpy(dst, src)
|
||||
#define memcpy_P(dst, src, n) memcpy(dst, src, n)
|
||||
#elif defined(__AVR__)
|
||||
// Arduino/Arduboy platform
|
||||
#include <avr/pgmspace.h>
|
||||
#else
|
||||
// Flipper Zero and other ARM platforms
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#define PROGMEM
|
||||
#define PSTR(s) (s)
|
||||
#define pgm_read_byte(x) (*((const uint8_t*)(x)))
|
||||
#define pgm_read_word(x) (*((const uint16_t*)(x)))
|
||||
#define pgm_read_dword(x) (*((const uint32_t*)(x)))
|
||||
#define strlen_P(x) strlen(x)
|
||||
#define strcpy_P(dst, src) strcpy(dst, src)
|
||||
#define strcmp_P(s1, s2) strcmp(s1, s2)
|
||||
#define strncmp_P(s1, s2, n) strncmp(s1, s2, n)
|
||||
#define memcpy_P(dst, src, n) memcpy(dst, src, n)
|
||||
#define sprintf_P sprintf
|
||||
#define snprintf_P snprintf
|
||||
#endif
|
||||
|
||||
// Display configuration
|
||||
#define DISPLAY_WIDTH 128
|
||||
#define DISPLAY_HEIGHT 64
|
||||
|
||||
// Game settings
|
||||
#define DEV_MODE 0
|
||||
|
||||
// Input definitions
|
||||
#define INPUT_LEFT 1
|
||||
#define INPUT_RIGHT 2
|
||||
#define INPUT_UP 4
|
||||
#define INPUT_DOWN 8
|
||||
#define INPUT_A 16
|
||||
#define INPUT_B 32
|
||||
|
||||
#ifndef COLOUR_WHITE
|
||||
#define COLOUR_WHITE 1
|
||||
#endif
|
||||
|
||||
#ifndef COLOUR_BLACK
|
||||
#define COLOUR_BLACK 0
|
||||
#endif
|
||||
|
||||
// Angle system (256 = 360 degrees)
|
||||
#define FIXED_ANGLE_MAX 256
|
||||
|
||||
// 3D rendering settings
|
||||
#define CAMERA_SCALE 1
|
||||
#define CLIP_PLANE 32
|
||||
#define CLIP_ANGLE 32
|
||||
#define NEAR_PLANE_MULTIPLIER 130
|
||||
#define NEAR_PLANE (DISPLAY_WIDTH * NEAR_PLANE_MULTIPLIER / 256)
|
||||
#define HORIZON (DISPLAY_HEIGHT / 2)
|
||||
|
||||
// World settings
|
||||
#define CELL_SIZE 256
|
||||
#define PARTICLES_PER_SYSTEM 8
|
||||
#define BASE_SPRITE_SIZE 16
|
||||
#define MAX_SPRITE_SIZE (DISPLAY_HEIGHT / 2)
|
||||
#define MIN_TEXTURE_DISTANCE 4
|
||||
#define MAX_QUEUED_DRAWABLES 12
|
||||
|
||||
// Player settings
|
||||
#define TURN_SPEED 3
|
||||
@@ -0,0 +1,146 @@
|
||||
#pragma once
|
||||
#include "game/Defines.h"
|
||||
|
||||
#define WITH_IMAGE_TEXTURES 0
|
||||
#define WITH_VECTOR_TEXTURES 1
|
||||
#define WITH_TEXTURES (WITH_IMAGE_TEXTURES || WITH_VECTOR_TEXTURES)
|
||||
#define WITH_SPRITE_OUTLINES 1
|
||||
|
||||
struct Camera {
|
||||
int16_t x, y;
|
||||
uint8_t angle;
|
||||
int16_t rotCos, rotSin;
|
||||
int16_t clipCos, clipSin;
|
||||
uint8_t cellX, cellY;
|
||||
int8_t tilt;
|
||||
int8_t bob;
|
||||
uint8_t shakeTime;
|
||||
};
|
||||
|
||||
enum class DrawableType : uint8_t {
|
||||
Sprite = 0,
|
||||
ParticleSystem = 1
|
||||
};
|
||||
|
||||
enum class AnchorType : uint8_t {
|
||||
Floor,
|
||||
Center,
|
||||
BelowCenter,
|
||||
Ceiling
|
||||
};
|
||||
|
||||
struct QueuedDrawable {
|
||||
union {
|
||||
const uint16_t* spriteData;
|
||||
struct ParticleSystem* particleSystem;
|
||||
};
|
||||
|
||||
DrawableType type : 1;
|
||||
bool invert : 1;
|
||||
int8_t x;
|
||||
int8_t y;
|
||||
uint8_t halfSize;
|
||||
uint8_t inverseCameraDistance;
|
||||
};
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
static Camera camera;
|
||||
static uint8_t wBuffer[DISPLAY_WIDTH];
|
||||
static uint8_t globalRenderFrame;
|
||||
|
||||
static void Render();
|
||||
|
||||
static void DrawObject(
|
||||
const uint16_t* spriteData,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t scale = 128,
|
||||
AnchorType anchor = AnchorType::Floor,
|
||||
bool invert = false);
|
||||
static QueuedDrawable* CreateQueuedDrawable(uint8_t inverseCameraDistance);
|
||||
static int8_t GetHorizon(int16_t x);
|
||||
|
||||
static bool
|
||||
TransformAndCull(int16_t worldX, int16_t worldY, int16_t& outScreenX, int16_t& outScreenW);
|
||||
|
||||
static void DrawScaled(
|
||||
const uint16_t* data,
|
||||
int8_t x,
|
||||
int8_t y,
|
||||
uint8_t halfSize,
|
||||
uint8_t inverseCameraDistance,
|
||||
bool invert = false,
|
||||
uint8_t color = COLOUR_BLACK);
|
||||
|
||||
static int8_t horizonBuffer[DISPLAY_WIDTH];
|
||||
static uint8_t numBufferSlicesFilled;
|
||||
static void DrawWallLine(
|
||||
int16_t x1,
|
||||
int16_t y1,
|
||||
int16_t x2,
|
||||
int16_t y2,
|
||||
uint8_t clipLeft,
|
||||
uint8_t clipRight,
|
||||
uint8_t col);
|
||||
static void DrawWallSegment(
|
||||
const uint8_t* texture,
|
||||
int16_t x1,
|
||||
int16_t w1,
|
||||
int16_t x2,
|
||||
int16_t w2,
|
||||
uint8_t u1clip,
|
||||
uint8_t u2clip,
|
||||
bool edgeLeft,
|
||||
bool edgeRight,
|
||||
bool shadeEdge);
|
||||
static void DrawWall(
|
||||
const uint8_t* texture,
|
||||
int16_t x1,
|
||||
int16_t y1,
|
||||
int16_t x2,
|
||||
int16_t y2,
|
||||
bool edgeLeft,
|
||||
bool edgeRight,
|
||||
bool shadeEdge);
|
||||
static void DrawBackground();
|
||||
static uint8_t numQueuedDrawables;
|
||||
static bool isFrustrumClipped(int16_t x, int16_t y);
|
||||
|
||||
private:
|
||||
static QueuedDrawable queuedDrawables[MAX_QUEUED_DRAWABLES];
|
||||
|
||||
// #if WITH_IMAGE_TEXTURES
|
||||
// static void DrawWallSegment(const uint16_t* texture, int16_t x1, int16_t w1, int16_t x2, int16_t w2, uint8_t u1clip, uint8_t u2clip, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// static void DrawWall(const uint16_t* texture, int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// #elif WITH_VECTOR_TEXTURES
|
||||
// static void DrawWallLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint8_t clipLeft, uint8_t clipRight, uint8_t col);
|
||||
// static void DrawWallSegment(const uint8_t* texture, int16_t x1, int16_t w1, int16_t x2, int16_t w2, uint8_t u1clip, uint8_t u2clip, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// static void DrawWall(const uint8_t* texture, int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// #else
|
||||
// static void DrawWallSegment(int16_t x1, int16_t w1, int16_t x2, int16_t w2, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// static void DrawWall(int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// #endif
|
||||
static void DrawFloorLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2);
|
||||
static void DrawFloorLineInner(int16_t x1, int16_t y1, int16_t x2, int16_t y2);
|
||||
static void DrawFloorLines();
|
||||
|
||||
static void TransformToViewSpace(int16_t x, int16_t y, int16_t& outX, int16_t& outY);
|
||||
static void TransformToScreenSpace(int16_t viewX, int16_t viewZ, int16_t& outX, int16_t& outW);
|
||||
|
||||
static void DrawCell(uint8_t x, uint8_t y);
|
||||
static void DrawCells();
|
||||
static void DrawWeapon();
|
||||
static void DrawHUD();
|
||||
static void DrawBar(uint8_t* screenPtr, const uint8_t* iconData, uint8_t amount, uint8_t max);
|
||||
static void DrawDamageIndicator();
|
||||
|
||||
static void QueueSprite(
|
||||
const uint16_t* data,
|
||||
int8_t x,
|
||||
int8_t y,
|
||||
uint8_t halfSize,
|
||||
uint8_t inverseCameraDistance,
|
||||
bool invert = false);
|
||||
static void RenderQueuedDrawables();
|
||||
};
|
||||
@@ -0,0 +1,484 @@
|
||||
#include "game/Enemy.h"
|
||||
#include "game/Defines.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/Projectile.h"
|
||||
#include "game/Generated/SpriteTypes.h"
|
||||
#include "game/Sounds.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Particle.h"
|
||||
|
||||
Enemy EnemyManager::enemies[maxEnemies];
|
||||
|
||||
const EnemyArchetype Enemy::archetypes[(int)EnemyType::NumEnemyTypes] PROGMEM =
|
||||
{
|
||||
{
|
||||
// Skeleton
|
||||
skeletonSpriteData,
|
||||
50, // hp
|
||||
4, // speed
|
||||
20, // attackStrength
|
||||
3, // attackDuration
|
||||
2, // stunDuration
|
||||
false, // isRanged
|
||||
96, // sprite scale
|
||||
AnchorType::Floor // sprite anchor
|
||||
},
|
||||
{
|
||||
// Mage
|
||||
mageSpriteData,
|
||||
30, // hp
|
||||
5, // speed
|
||||
20, // attackStrength
|
||||
3, // attackDuration
|
||||
2, // stunDuration
|
||||
true, // isRanged
|
||||
96, // sprite scale
|
||||
AnchorType::Floor // sprite anchor
|
||||
},
|
||||
{
|
||||
// Bat -> shotgun sergeant (ranged, hits hard)
|
||||
batSpriteData,
|
||||
20, // hp
|
||||
6, // speed
|
||||
10, // attackStrength
|
||||
2, // attackDuration
|
||||
0, // stunDuration
|
||||
true, // isRanged
|
||||
90, // sprite scale
|
||||
AnchorType::Floor // sprite anchor
|
||||
},
|
||||
{
|
||||
// Spider -> zombieman (ranged, weak)
|
||||
spiderSpriteData,
|
||||
10, // hp
|
||||
5, // speed
|
||||
4, // attackStrength
|
||||
1, // attackDuration
|
||||
0, // stunDuration
|
||||
true, // isRanged
|
||||
80, // sprite scale
|
||||
AnchorType::Floor // sprite anchor
|
||||
}
|
||||
};
|
||||
|
||||
void Enemy::Init(EnemyType initType, int16_t initX, int16_t initY)
|
||||
{
|
||||
state = EnemyState::Idle;
|
||||
type = initType;
|
||||
x = initX;
|
||||
y = initY;
|
||||
frameDelay = 0;
|
||||
targetCellX = x / CELL_SIZE;
|
||||
targetCellY = y / CELL_SIZE;
|
||||
hp = GetArchetype()->GetHP();
|
||||
}
|
||||
|
||||
void Enemy::Damage(uint8_t amount)
|
||||
{
|
||||
if (amount >= hp)
|
||||
{
|
||||
Game::stats.enemyKills[(int)type]++;
|
||||
type = EnemyType::None;
|
||||
Platform::PlaySound(Sounds::Kill);
|
||||
ParticleSystemManager::CreateExplosion(x, y, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
hp -= amount;
|
||||
Platform::PlaySound(Sounds::Hit);
|
||||
state = EnemyState::Stunned;
|
||||
frameDelay = GetArchetype()->GetStunDuration();
|
||||
}
|
||||
}
|
||||
|
||||
const EnemyArchetype* Enemy::GetArchetype() const
|
||||
{
|
||||
if (type == EnemyType::None)
|
||||
return nullptr;
|
||||
return &archetypes[(int)type];
|
||||
}
|
||||
|
||||
int16_t Clamp(int16_t x, int16_t min, int16_t max)
|
||||
{
|
||||
if(x < min)
|
||||
return min;
|
||||
if(x > max)
|
||||
return max;
|
||||
return x;
|
||||
}
|
||||
|
||||
bool Enemy::TryPickCell(int8_t newX, int8_t newY)
|
||||
{
|
||||
if(Map::IsBlocked(newX, newY))// && !engine.map.isDoor(newX, newZ))
|
||||
return false;
|
||||
if(Map::IsBlocked(targetCellX, newY)) // && !engine.map.isDoor(targetCellX, newZ))
|
||||
return false;
|
||||
if(Map::IsBlocked(newX, targetCellY)) // && !engine.map.isDoor(newX, targetCellZ))
|
||||
return false;
|
||||
|
||||
for (Enemy& other : EnemyManager::enemies)
|
||||
{
|
||||
if(this != &other && other.IsValid())
|
||||
{
|
||||
if(other.targetCellX == newX && other.targetCellY == newY)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
targetCellX = newX;
|
||||
targetCellY = newY;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Enemy::TryPickCells(int8_t deltaX, int8_t deltaY)
|
||||
{
|
||||
return TryPickCell(targetCellX + deltaX, targetCellY + deltaY)
|
||||
|| TryPickCell(targetCellX + deltaX, targetCellY)
|
||||
|| TryPickCell(targetCellX, targetCellY + deltaY)
|
||||
|| TryPickCell(targetCellX - deltaX, targetCellY + deltaY)
|
||||
|| TryPickCell(targetCellX + deltaX, targetCellY - deltaY);
|
||||
}
|
||||
|
||||
uint8_t Enemy::GetPlayerCellDistance() const
|
||||
{
|
||||
uint8_t dx = ABS(Game::player.x - x) / CELL_SIZE;
|
||||
uint8_t dy = ABS(Game::player.y - y) / CELL_SIZE;
|
||||
return dx > dy ? dx : dy;
|
||||
}
|
||||
|
||||
void Enemy::PickNewTargetCell()
|
||||
{
|
||||
int8_t deltaX = (int8_t) Clamp((Game::player.x / CELL_SIZE) - targetCellX, -1, 1);
|
||||
int8_t deltaY = (int8_t) Clamp((Game::player.y / CELL_SIZE) - targetCellY, -1, 1);
|
||||
uint8_t dodgeChance = (uint8_t) Random();
|
||||
|
||||
if (GetArchetype()->GetIsRanged() && GetPlayerCellDistance() < 3)
|
||||
{
|
||||
deltaX = -deltaX;
|
||||
deltaY = -deltaY;
|
||||
}
|
||||
|
||||
if(deltaX == 0)
|
||||
{
|
||||
if(dodgeChance < 64)
|
||||
{
|
||||
deltaX = -1;
|
||||
}
|
||||
else if(dodgeChance < 128)
|
||||
{
|
||||
deltaX = 1;
|
||||
}
|
||||
}
|
||||
else if(deltaY == 0)
|
||||
{
|
||||
if(dodgeChance < 64)
|
||||
{
|
||||
deltaY = -1;
|
||||
}
|
||||
else if(dodgeChance < 128)
|
||||
{
|
||||
deltaY = 1;
|
||||
}
|
||||
}
|
||||
|
||||
TryPickCells(deltaX, deltaY);
|
||||
}
|
||||
|
||||
void Enemy::StunMove()
|
||||
{
|
||||
//int16_t targetX = Game::player.x;
|
||||
//int16_t targetY = Game::player.y;
|
||||
//
|
||||
//int16_t maxDelta = 3;
|
||||
//
|
||||
//int16_t deltaX = Clamp(targetX - x, -maxDelta, maxDelta);
|
||||
//int16_t deltaY = Clamp(targetY - y, -maxDelta, maxDelta);
|
||||
//
|
||||
//x -= deltaX;
|
||||
//y -= deltaY;
|
||||
|
||||
// int16_t deltaX = (Random() % 16) - 8;
|
||||
// int16_t deltaY = (Random() % 16) - 8;
|
||||
// x += deltaX;
|
||||
// y += deltaY;
|
||||
}
|
||||
|
||||
bool Enemy::TryMove()
|
||||
{
|
||||
if(Map::IsSolid(targetCellX, targetCellY))
|
||||
{
|
||||
//engine.map.openDoorsAt(targetCellX, targetCellZ, Direction_None);
|
||||
return false;
|
||||
}
|
||||
|
||||
int16_t targetX = (targetCellX * CELL_SIZE) + CELL_SIZE / 2;
|
||||
int16_t targetY = (targetCellY * CELL_SIZE) + CELL_SIZE / 2;
|
||||
|
||||
int16_t maxDelta = GetArchetype()->GetMovementSpeed();
|
||||
|
||||
int16_t deltaX = Clamp(targetX - x, -maxDelta, maxDelta);
|
||||
int16_t deltaY = Clamp(targetY - y, -maxDelta, maxDelta);
|
||||
|
||||
x += deltaX;
|
||||
y += deltaY;
|
||||
|
||||
if(IsOverlappingEntity(Game::player))
|
||||
{
|
||||
if (!GetArchetype()->GetIsRanged())
|
||||
{
|
||||
Game::player.Damage(GetArchetype()->GetAttackStrength());
|
||||
if (Game::player.hp == 0)
|
||||
{
|
||||
Game::stats.killedBy = type;
|
||||
}
|
||||
|
||||
state = EnemyState::Attacking;
|
||||
frameDelay = GetArchetype()->GetAttackDuration();
|
||||
}
|
||||
|
||||
x -= deltaX;
|
||||
y -= deltaY;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(x == targetX && y == targetY)
|
||||
{
|
||||
PickNewTargetCell();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Enemy::FireProjectile(uint8_t angle)
|
||||
{
|
||||
return ProjectileManager::FireProjectile(this, x, y, angle) != nullptr;
|
||||
}
|
||||
|
||||
bool Enemy::TryFireProjectile()
|
||||
{
|
||||
int8_t deltaX = (Game::player.x - x) / CELL_SIZE;
|
||||
int8_t deltaY = (Game::player.y - y) / CELL_SIZE;
|
||||
|
||||
if (deltaX == 0)
|
||||
{
|
||||
if (deltaY < 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_270);
|
||||
}
|
||||
else if (deltaY > 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_90);
|
||||
}
|
||||
}
|
||||
else if (deltaY == 0)
|
||||
{
|
||||
if (deltaX < 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_180);
|
||||
}
|
||||
else if (deltaX > 0)
|
||||
{
|
||||
return FireProjectile(0);
|
||||
}
|
||||
}
|
||||
else if (deltaX == deltaY)
|
||||
{
|
||||
if (deltaX > 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_45);
|
||||
}
|
||||
else
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_180 + FIXED_ANGLE_45);
|
||||
}
|
||||
}
|
||||
else if (deltaX == -deltaY)
|
||||
{
|
||||
if (deltaX > 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_270 + FIXED_ANGLE_45);
|
||||
}
|
||||
else
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_90 + FIXED_ANGLE_45);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Enemy::ShouldFireProjectile() const
|
||||
{
|
||||
uint8_t distance = GetPlayerCellDistance();
|
||||
uint8_t chance = 16 / (distance > 0 ? distance : 1);
|
||||
|
||||
return GetArchetype()->GetIsRanged() && (Random() & 0xff) < chance && Map::IsClearLine(x, y, Game::player.x, Game::player.y);
|
||||
}
|
||||
|
||||
void Enemy::Tick()
|
||||
{
|
||||
if (state == EnemyState::Stunned)
|
||||
{
|
||||
StunMove();
|
||||
}
|
||||
|
||||
if (frameDelay > 0)
|
||||
{
|
||||
if ((Game::globalTickFrame & 0xf) == 0)
|
||||
{
|
||||
frameDelay--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case EnemyState::Idle:
|
||||
if (Map::IsClearLine(x, y, Game::player.x, Game::player.y))
|
||||
{
|
||||
Platform::PlaySound(Sounds::SpotPlayer);
|
||||
state = EnemyState::Moving;
|
||||
}
|
||||
break;
|
||||
case EnemyState::Moving:
|
||||
TryMove();
|
||||
|
||||
if (ShouldFireProjectile())
|
||||
{
|
||||
if (TryFireProjectile())
|
||||
{
|
||||
Platform::PlaySound(Sounds::Shoot);
|
||||
state = EnemyState::Attacking;
|
||||
frameDelay = GetArchetype()->GetAttackDuration();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EnemyState::Attacking:
|
||||
state = EnemyState::Moving;
|
||||
break;
|
||||
case EnemyState::Stunned:
|
||||
state = EnemyState::Moving;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EnemyManager::Init()
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
enemy.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void EnemyManager::Update()
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
if(enemy.IsValid())
|
||||
{
|
||||
enemy.Tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EnemyManager::Draw()
|
||||
{
|
||||
for(Enemy& enemy : enemies)
|
||||
{
|
||||
if(enemy.IsValid())
|
||||
{
|
||||
bool invert = enemy.GetState() == EnemyState::Stunned && (Renderer::globalRenderFrame & 1);
|
||||
int frameOffset = (enemy.GetType() == EnemyType::Bat || enemy.GetState() == EnemyState::Moving) && (Game::globalTickFrame & 8) == 0 ? 32 : 0;
|
||||
|
||||
const EnemyArchetype* archetype = enemy.GetArchetype();
|
||||
Renderer::DrawObject(archetype->GetSpriteData() + frameOffset, enemy.x, enemy.y, archetype->GetSpriteScale(), archetype->GetSpriteAnchor(), invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EnemyManager::Spawn(EnemyType enemyType, int16_t x, int16_t y)
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
if(!enemy.IsValid())
|
||||
{
|
||||
enemy.Init(enemyType, x, y);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Doom-like difficulty curve: early levels are mostly zombiemen,
|
||||
// deeper levels bring sergeants, imps and finally demons
|
||||
static EnemyType PickEnemyType()
|
||||
{
|
||||
uint8_t roll = (uint8_t)(Random() % 100);
|
||||
uint8_t f = Game::floor;
|
||||
|
||||
if (f < 3)
|
||||
{
|
||||
if (roll < 55) return EnemyType::Spider; // zombieman
|
||||
if (roll < 85) return EnemyType::Bat; // sergeant
|
||||
return EnemyType::Mage; // imp
|
||||
}
|
||||
if (f < 6)
|
||||
{
|
||||
if (roll < 30) return EnemyType::Spider;
|
||||
if (roll < 55) return EnemyType::Bat;
|
||||
if (roll < 85) return EnemyType::Mage;
|
||||
return EnemyType::Skeleton; // demon
|
||||
}
|
||||
if (roll < 15) return EnemyType::Spider;
|
||||
if (roll < 40) return EnemyType::Bat;
|
||||
if (roll < 70) return EnemyType::Mage;
|
||||
return EnemyType::Skeleton;
|
||||
}
|
||||
|
||||
void EnemyManager::SpawnEnemies()
|
||||
{
|
||||
for (uint8_t y = 0; y < Map::height; y++)
|
||||
{
|
||||
for (uint8_t x = 0; x < Map::width; x++)
|
||||
{
|
||||
if (Map::GetCellSafe(x, y) == CellType::Monster)
|
||||
{
|
||||
EnemyManager::Spawn(PickEnemyType(), x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2);
|
||||
Map::SetCell(x, y, CellType::Empty);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Enemy* EnemyManager::GetOverlappingEnemy(Entity& entity)
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
if (enemy.IsValid() && enemy.IsOverlappingEntity(entity))
|
||||
{
|
||||
return &enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Enemy* EnemyManager::GetOverlappingEnemy(int16_t x, int16_t y)
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
if (enemy.IsValid() && enemy.IsOverlappingPoint(x, y))
|
||||
{
|
||||
return &enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Entity.h"
|
||||
#include "game/Defines.h"
|
||||
#include "game/Draw.h"
|
||||
|
||||
enum class EnemyType : uint8_t
|
||||
{
|
||||
Skeleton,
|
||||
Mage,
|
||||
Bat,
|
||||
Spider,
|
||||
NumEnemyTypes,
|
||||
None = NumEnemyTypes,
|
||||
Exit,
|
||||
};
|
||||
|
||||
enum class EnemyState : uint8_t
|
||||
{
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Stunned,
|
||||
Dying,
|
||||
Dead
|
||||
};
|
||||
|
||||
struct EnemyArchetype
|
||||
{
|
||||
const uint16_t* spriteData;
|
||||
|
||||
uint8_t hp;
|
||||
uint8_t movementSpeed;
|
||||
uint8_t attackStrength;
|
||||
uint8_t attackDuration;
|
||||
uint8_t stunDuration;
|
||||
uint8_t isRanged;
|
||||
uint8_t spriteScale;
|
||||
AnchorType spriteAnchor;
|
||||
|
||||
const uint16_t* GetSpriteData() const {return static_cast<const uint16_t*>(pgm_read_ptr_safe(&spriteData));}
|
||||
uint8_t GetHP() const { return pgm_read_byte(&hp); }
|
||||
uint8_t GetMovementSpeed() const { return pgm_read_byte(&movementSpeed); }
|
||||
uint8_t GetAttackStrength() const { return pgm_read_byte(&attackStrength); }
|
||||
uint8_t GetAttackDuration() const { return pgm_read_byte(&attackDuration); }
|
||||
uint8_t GetStunDuration() const { return pgm_read_byte(&stunDuration); }
|
||||
bool GetIsRanged() const { return pgm_read_byte(&isRanged) != 0; }
|
||||
uint8_t GetSpriteScale() const { return pgm_read_byte(&spriteScale); }
|
||||
AnchorType GetSpriteAnchor() const { return (AnchorType) pgm_read_byte(&spriteAnchor); }
|
||||
};
|
||||
|
||||
class Enemy : public Entity
|
||||
{
|
||||
public:
|
||||
void Init(EnemyType type, int16_t x, int16_t y);
|
||||
void Tick();
|
||||
bool IsValid() const { return type != EnemyType::None; }
|
||||
void Damage(uint8_t amount);
|
||||
void Clear() { type = EnemyType::None; }
|
||||
const EnemyArchetype* GetArchetype() const;
|
||||
EnemyState GetState() const { return state; }
|
||||
EnemyType GetType() const { return type; }
|
||||
|
||||
private:
|
||||
static const EnemyArchetype archetypes[(int)EnemyType::NumEnemyTypes];
|
||||
|
||||
bool ShouldFireProjectile() const;
|
||||
bool FireProjectile(uint8_t angle);
|
||||
bool TryMove();
|
||||
void StunMove();
|
||||
bool TryFireProjectile();
|
||||
void PickNewTargetCell();
|
||||
bool TryPickCells(int8_t deltaX, int8_t deltaY);
|
||||
bool TryPickCell(int8_t newX, int8_t newY);
|
||||
uint8_t GetPlayerCellDistance() const;
|
||||
|
||||
EnemyType type : 3;
|
||||
EnemyState state : 3;
|
||||
uint8_t frameDelay : 2;
|
||||
uint8_t hp;
|
||||
uint8_t targetCellX, targetCellY;
|
||||
};
|
||||
|
||||
class EnemyManager
|
||||
{
|
||||
public:
|
||||
static constexpr int maxEnemies = 24; //24;
|
||||
static Enemy enemies[maxEnemies];
|
||||
|
||||
static void Spawn(EnemyType enemyType, int16_t x, int16_t y);
|
||||
static void SpawnEnemies();
|
||||
|
||||
static Enemy* GetOverlappingEnemy(Entity& entity);
|
||||
static Enemy* GetOverlappingEnemy(int16_t x, int16_t y);
|
||||
|
||||
static void Init();
|
||||
static void Draw();
|
||||
static void Update();
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "game/Entity.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/Map.h"
|
||||
|
||||
#define ENTITY_SIZE 192
|
||||
|
||||
bool Entity::IsOverlappingEntity(const Entity& other) const
|
||||
{
|
||||
return (x >= other.x - ENTITY_SIZE && x <= other.x + ENTITY_SIZE
|
||||
&& y >= other.y - ENTITY_SIZE && y <= other.y + ENTITY_SIZE);
|
||||
}
|
||||
|
||||
bool Entity::IsOverlappingPoint(int16_t pointX, int16_t pointY) const
|
||||
{
|
||||
return (pointX >= x - ENTITY_SIZE / 2 && pointX <= x + ENTITY_SIZE / 2
|
||||
&& pointY >= y - ENTITY_SIZE / 2 && pointY <= y + ENTITY_SIZE / 2);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class Entity
|
||||
{
|
||||
public:
|
||||
bool IsOverlappingPoint(int16_t pointX, int16_t pointY) const;
|
||||
bool IsOverlappingEntity(const Entity& other) const;
|
||||
|
||||
int16_t x, y;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// #include "FixedMath.h"
|
||||
|
||||
// static uint16_t xs = 1;
|
||||
|
||||
// uint16_t Random() {
|
||||
// xs ^= xs << 7;
|
||||
// xs ^= xs >> 9;
|
||||
// xs ^= xs << 8;
|
||||
// return xs;
|
||||
// }
|
||||
|
||||
// void SeedRandom() {
|
||||
// uint32_t r = furi_hal_random_get();
|
||||
// xs = (uint16_t)(r ^ (r >> 16));
|
||||
// }
|
||||
|
||||
// void SeedRandom(uint16_t seed) {
|
||||
// xs = seed | 1;
|
||||
// }
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#if defined(_WIN32)
|
||||
#include <math.h>
|
||||
#endif
|
||||
#include "game/Defines.h"
|
||||
|
||||
#define FIXED_SHIFT 8
|
||||
#define FIXED_ONE (1 << FIXED_SHIFT)
|
||||
#define INT_TO_FIXED(x) ((x) * FIXED_ONE)
|
||||
#define FIXED_TO_INT(x) ((x) >> 8)
|
||||
#define FLOAT_TO_FIXED(x) ((int16_t)((x) * FIXED_ONE))
|
||||
|
||||
#ifndef ABS
|
||||
#define ABS(x) (((x) < 0) ? -(x) : (x))
|
||||
#endif
|
||||
|
||||
#define FIXED_ANGLE_MAX 256
|
||||
#define FIXED_ANGLE_WRAP(x) ((x) & 255)
|
||||
#define FIXED_ANGLE_45 (FIXED_ANGLE_90 / 2)
|
||||
#define FIXED_ANGLE_90 (FIXED_ANGLE_MAX / 4)
|
||||
#define FIXED_ANGLE_180 (FIXED_ANGLE_90 * 2)
|
||||
#define FIXED_ANGLE_270 (FIXED_ANGLE_90 * 3)
|
||||
#define FIXED_ANGLE_TO_RADIANS(x) ((x) * (2.0f * 3.141592654f / FIXED_ANGLE_MAX))
|
||||
|
||||
extern const int16_t sinTable[FIXED_ANGLE_MAX] PROGMEM;
|
||||
|
||||
inline int16_t FixedSin(uint8_t angle) {
|
||||
return pgm_read_word(&sinTable[angle]);
|
||||
}
|
||||
|
||||
inline int16_t FixedCos(uint8_t angle) {
|
||||
return pgm_read_word(&sinTable[FIXED_ANGLE_WRAP(FIXED_ANGLE_90 - angle)]);
|
||||
}
|
||||
|
||||
// uint16_t Random();
|
||||
// void SeedRandom();
|
||||
// void SeedRandom(uint16_t seed);
|
||||
|
||||
inline uint16_t Random() {
|
||||
uint32_t r = furi_hal_random_get();
|
||||
return (uint16_t)(r ^ (r >> 16));
|
||||
}
|
||||
@@ -0,0 +1,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();
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// 8x8, 8 кадров (0..7) — как в первом коде
|
||||
static const uint8_t PROGMEM transitionSet[] = {
|
||||
8,
|
||||
8,
|
||||
// FRAME 00
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
// FRAME 01
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
// FRAME 02
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
// FRAME 03
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
// FRAME 04
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
// FRAME 05
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
// FRAME 06
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
// FRAME 07
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
};
|
||||
|
||||
class Menu {
|
||||
public:
|
||||
void Init();
|
||||
void Draw();
|
||||
void Tick();
|
||||
|
||||
void ReadSave();
|
||||
void WriteSave();
|
||||
|
||||
void TickEnteringLevel();
|
||||
void DrawEnteringLevel();
|
||||
void TransitionToLevel();
|
||||
|
||||
void TickGameOver();
|
||||
void DrawGameOver();
|
||||
|
||||
void ResetTimer();
|
||||
void FadeOut();
|
||||
|
||||
private:
|
||||
using TransitionNextFn = void (*)();
|
||||
static void RunTransition(Menu* menu, uint8_t& t, TransitionNextFn next);
|
||||
void DrawTransitionFrame(uint8_t frameIndex);
|
||||
|
||||
void SetScore(uint16_t score);
|
||||
void PrintItem(uint8_t idx, uint8_t row);
|
||||
|
||||
uint8_t m_selection = 0;
|
||||
uint8_t m_topIndex = 0;
|
||||
uint8_t m_cursorPos = 0;
|
||||
|
||||
uint16_t m_score = 0;
|
||||
uint16_t m_high = 0;
|
||||
uint16_t m_storedHigh = 0;
|
||||
uint8_t m_save[9] = {0};
|
||||
|
||||
union {
|
||||
uint16_t timer;
|
||||
uint16_t fizzleFade;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "game/Particle.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Platform.h"
|
||||
|
||||
ParticleSystem ParticleSystemManager::systems[MAX_SYSTEMS];
|
||||
|
||||
void ParticleSystem::Init()
|
||||
{
|
||||
life = 0;
|
||||
}
|
||||
|
||||
void ParticleSystem::Step()
|
||||
{
|
||||
for (Particle& p : particles)
|
||||
{
|
||||
if (p.IsActive())
|
||||
{
|
||||
p.velY += gravity;
|
||||
|
||||
if (p.x + p.velX < -127 || p.x + p.velX > 127 || p.y + p.velY < -127)
|
||||
{
|
||||
p.x = -128;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (p.y + p.velY >= 128)
|
||||
{
|
||||
p.velY = p.velX = 0;
|
||||
p.y = 127;
|
||||
}
|
||||
|
||||
p.x += p.velX;
|
||||
p.y += p.velY;
|
||||
}
|
||||
}
|
||||
|
||||
life--;
|
||||
}
|
||||
|
||||
void ParticleSystem::Draw(int x, int halfScale)
|
||||
{
|
||||
int scale = 2 * halfScale;
|
||||
int8_t horizon = Renderer::GetHorizon(x);
|
||||
uint8_t colour = isWhite ? COLOUR_WHITE : COLOUR_BLACK;
|
||||
|
||||
for (Particle& p : particles)
|
||||
{
|
||||
if (p.IsActive())
|
||||
{
|
||||
int outX = x + ((p.x * scale) >> 8);
|
||||
int outY = horizon + ((p.y * scale) >> 8);
|
||||
|
||||
if (outX >= 0 && outY >= 0 && outX < DISPLAY_WIDTH - 1 && outY < DISPLAY_HEIGHT - 1 && halfScale >= Renderer::wBuffer[outX])
|
||||
{
|
||||
Platform::PutPixel(outX, outY, colour);
|
||||
Platform::PutPixel(outX + 1, outY, colour);
|
||||
Platform::PutPixel(outX + 1, outY + 1, colour);
|
||||
Platform::PutPixel(outX, outY + 1, colour);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystem::Explode()
|
||||
{
|
||||
for (Particle& p : particles)
|
||||
{
|
||||
p.x = (Random() & 31) - 16;
|
||||
p.y = (Random() & 31) - 16;
|
||||
|
||||
p.velX = (Random() & 31) - 16;
|
||||
p.velY = (Random() & 31) - 25;
|
||||
}
|
||||
|
||||
life = 22;
|
||||
}
|
||||
|
||||
void ParticleSystemManager::Draw()
|
||||
{
|
||||
for (ParticleSystem& system : systems)
|
||||
{
|
||||
if(system.IsActive())
|
||||
{
|
||||
int16_t screenX, screenW;
|
||||
|
||||
if(Renderer::TransformAndCull(system.worldX, system.worldY, screenX, screenW))
|
||||
{
|
||||
QueuedDrawable* drawable = Renderer::CreateQueuedDrawable((uint8_t)screenW);
|
||||
if(drawable)
|
||||
{
|
||||
drawable->type = DrawableType::ParticleSystem;
|
||||
drawable->x = (int8_t)screenX;
|
||||
drawable->inverseCameraDistance = (uint8_t)screenW;
|
||||
drawable->particleSystem = &system;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystemManager::Init()
|
||||
{
|
||||
for (ParticleSystem& system : systems)
|
||||
{
|
||||
system.Init();
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystemManager::Update()
|
||||
{
|
||||
for (ParticleSystem& system : systems)
|
||||
{
|
||||
if(system.IsActive())
|
||||
{
|
||||
system.Step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystemManager::CreateExplosion(int16_t worldX, int16_t worldY, bool isWhite)
|
||||
{
|
||||
ParticleSystem* newSystem = nullptr;
|
||||
for(ParticleSystem& system : systems)
|
||||
{
|
||||
if(!system.IsActive())
|
||||
{
|
||||
newSystem = &system;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!newSystem)
|
||||
{
|
||||
newSystem = &systems[0];
|
||||
|
||||
for (uint8_t n = 1; n < MAX_SYSTEMS; n++)
|
||||
{
|
||||
if (systems[n].life < newSystem->life)
|
||||
{
|
||||
newSystem = &systems[n];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newSystem->worldX = worldX;
|
||||
newSystem->worldY = worldY;
|
||||
newSystem->isWhite = isWhite;
|
||||
newSystem->Explode();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Defines.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/Game.h"
|
||||
|
||||
struct Particle
|
||||
{
|
||||
int8_t x, y;
|
||||
int8_t velX, velY;
|
||||
|
||||
inline bool IsActive() { return x != -128; }
|
||||
};
|
||||
|
||||
struct ParticleSystem
|
||||
{
|
||||
static constexpr int8_t gravity = 3;
|
||||
int16_t worldX, worldY;
|
||||
bool isWhite : 1;
|
||||
uint8_t life : 7;
|
||||
Particle particles[PARTICLES_PER_SYSTEM];
|
||||
|
||||
bool IsActive() { return life > 0; }
|
||||
|
||||
void Init();
|
||||
void Step();
|
||||
void Draw(int x, int scale);
|
||||
void Explode();
|
||||
};
|
||||
|
||||
class ParticleSystemManager
|
||||
{
|
||||
public:
|
||||
static constexpr int MAX_SYSTEMS = 3;
|
||||
static ParticleSystem systems[MAX_SYSTEMS];
|
||||
|
||||
static void Init();
|
||||
static void Draw();
|
||||
static void Update();
|
||||
static void CreateExplosion(int16_t x, int16_t y, bool isWhite = false);
|
||||
};
|
||||
@@ -0,0 +1,365 @@
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <notification/notification.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "lib/flipper.h"
|
||||
|
||||
#define COLOUR_WHITE 0
|
||||
#define COLOUR_BLACK 1
|
||||
|
||||
#include "game/Game.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Defines.h"
|
||||
#include "game/Sounds.h"
|
||||
#include "lib/EEPROM.h"
|
||||
|
||||
// ---------------- SOUND ----------------
|
||||
|
||||
typedef struct {
|
||||
const uint16_t* pattern;
|
||||
} SoundRequest;
|
||||
|
||||
static FuriMessageQueue* g_sound_queue = NULL;
|
||||
static FuriThread* g_sound_thread = NULL;
|
||||
static volatile bool g_sound_thread_running = false;
|
||||
static const float kSoundVolume = 1.0f;
|
||||
static const uint32_t kToneTickHz = 780;
|
||||
|
||||
static inline uint32_t arduboy_ticks_to_ms(uint16_t ticks) {
|
||||
return (uint32_t)((ticks * 1000u + (kToneTickHz / 2)) / kToneTickHz);
|
||||
}
|
||||
|
||||
static int32_t sound_thread_fn(void* ctx) {
|
||||
UNUSED(ctx);
|
||||
SoundRequest req;
|
||||
|
||||
while(g_sound_thread_running) {
|
||||
if(furi_message_queue_get(g_sound_queue, &req, 50) != FuriStatusOk) continue;
|
||||
if(!g_state || !g_state->audio_enabled || !req.pattern) continue;
|
||||
if(!furi_hal_speaker_acquire(50)) continue;
|
||||
|
||||
const uint16_t* p = req.pattern;
|
||||
while(g_sound_thread_running && g_state && g_state->audio_enabled) {
|
||||
SoundRequest new_req;
|
||||
if(furi_message_queue_get(g_sound_queue, &new_req, 0) == FuriStatusOk) {
|
||||
if(new_req.pattern) p = new_req.pattern;
|
||||
}
|
||||
|
||||
uint16_t freq = *p++;
|
||||
if(freq == TONES_END) break;
|
||||
|
||||
uint16_t dur_ticks = *p++;
|
||||
uint32_t dur_ms = arduboy_ticks_to_ms(dur_ticks);
|
||||
if(dur_ms == 0) dur_ms = 1;
|
||||
|
||||
if(freq == 0) {
|
||||
furi_hal_speaker_stop();
|
||||
furi_delay_ms(dur_ms);
|
||||
} else {
|
||||
furi_hal_speaker_start((float)freq, kSoundVolume);
|
||||
furi_delay_ms(dur_ms);
|
||||
furi_hal_speaker_stop();
|
||||
}
|
||||
}
|
||||
|
||||
furi_hal_speaker_stop();
|
||||
furi_hal_speaker_release();
|
||||
}
|
||||
|
||||
if(furi_hal_speaker_is_mine()) {
|
||||
furi_hal_speaker_stop();
|
||||
furi_hal_speaker_release();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sound_system_init() {
|
||||
if(g_sound_queue || g_sound_thread) return;
|
||||
|
||||
g_sound_queue = furi_message_queue_alloc(4, sizeof(SoundRequest));
|
||||
|
||||
g_sound_thread = furi_thread_alloc();
|
||||
furi_thread_set_name(g_sound_thread, "GameSound");
|
||||
furi_thread_set_stack_size(g_sound_thread, 1024);
|
||||
furi_thread_set_priority(g_sound_thread, FuriThreadPriorityNormal);
|
||||
furi_thread_set_callback(g_sound_thread, sound_thread_fn);
|
||||
|
||||
g_sound_thread_running = true;
|
||||
furi_thread_start(g_sound_thread);
|
||||
}
|
||||
|
||||
static void sound_system_deinit() {
|
||||
if(!g_sound_thread) return;
|
||||
|
||||
g_sound_thread_running = false;
|
||||
furi_thread_join(g_sound_thread);
|
||||
furi_thread_free(g_sound_thread);
|
||||
g_sound_thread = NULL;
|
||||
|
||||
if(g_sound_queue) {
|
||||
furi_message_queue_free(g_sound_queue);
|
||||
g_sound_queue = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void Platform::PlaySound(const uint16_t* audioPattern) {
|
||||
if(!g_state || !g_state->audio_enabled || !audioPattern || !g_sound_queue) return;
|
||||
|
||||
SoundRequest req = {.pattern = audioPattern};
|
||||
if(furi_message_queue_put(g_sound_queue, &req, 0) != FuriStatusOk) {
|
||||
SoundRequest dummy;
|
||||
(void)furi_message_queue_get(g_sound_queue, &dummy, 0);
|
||||
(void)furi_message_queue_put(g_sound_queue, &req, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool Platform::IsAudioEnabled() {
|
||||
return g_state && g_state->audio_enabled;
|
||||
}
|
||||
|
||||
void Platform::SetAudioEnabled(bool enabled) {
|
||||
if(!g_state) return;
|
||||
bool was_enabled = g_state->audio_enabled;
|
||||
g_state->audio_enabled = enabled;
|
||||
if(enabled && !was_enabled)
|
||||
sound_system_init();
|
||||
else if(!enabled && was_enabled)
|
||||
sound_system_deinit();
|
||||
}
|
||||
|
||||
// ---------------- INPUT ----------------
|
||||
|
||||
uint8_t Platform::GetInput() {
|
||||
return g_state ? g_state->input_state : 0;
|
||||
}
|
||||
|
||||
// ---------------- DRAW ----------------
|
||||
|
||||
static constexpr uint8_t kDisplayPages = DISPLAY_HEIGHT / 8;
|
||||
|
||||
static inline int16_t floor_div8(int16_t value) {
|
||||
return (value >= 0) ? (value >> 3) : (int16_t)(-(((-value) + 7) >> 3));
|
||||
}
|
||||
|
||||
static inline void set_pixel(int16_t x, int16_t y, bool color) {
|
||||
if(!g_state) return;
|
||||
if((uint16_t)x >= DISPLAY_WIDTH || (uint16_t)y >= DISPLAY_HEIGHT) return;
|
||||
|
||||
uint8_t* buf = g_state->back_buffer;
|
||||
uint16_t idx = (uint16_t)(x + (y >> 3) * DISPLAY_WIDTH);
|
||||
uint8_t mask = (uint8_t)(1u << (y & 7));
|
||||
|
||||
if(color)
|
||||
buf[idx] |= mask;
|
||||
else
|
||||
buf[idx] &= (uint8_t)~mask;
|
||||
}
|
||||
|
||||
void Platform::PutPixel(uint8_t x, uint8_t y, uint8_t color) {
|
||||
set_pixel(x, y, color);
|
||||
}
|
||||
|
||||
void Platform::FillScreen(uint8_t color) {
|
||||
if(!g_state) return;
|
||||
memset(g_state->back_buffer, color ? 0xFF : 0x00, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
uint8_t* Platform::GetScreenBuffer() {
|
||||
return g_state ? g_state->back_buffer : NULL;
|
||||
}
|
||||
|
||||
void Platform::DrawVLine(uint8_t x, int8_t y0, int8_t y1, uint8_t pattern) {
|
||||
if(!g_state || pattern == 0 || x >= DISPLAY_WIDTH) return;
|
||||
|
||||
int16_t top = y0;
|
||||
int16_t bottom = y1;
|
||||
|
||||
if(top > bottom) {
|
||||
int16_t t = top;
|
||||
top = bottom;
|
||||
bottom = t;
|
||||
}
|
||||
|
||||
if(bottom < 0 || top >= DISPLAY_HEIGHT) return;
|
||||
|
||||
if(top < 0) top = 0;
|
||||
if(bottom >= DISPLAY_HEIGHT) bottom = DISPLAY_HEIGHT - 1;
|
||||
|
||||
uint8_t start_page = (uint8_t)(top >> 3);
|
||||
uint8_t end_page = (uint8_t)(bottom >> 3);
|
||||
uint8_t start_bit = (uint8_t)(top & 7);
|
||||
uint8_t end_bit = (uint8_t)(bottom & 7);
|
||||
|
||||
uint8_t* buf = g_state->back_buffer;
|
||||
|
||||
if(start_page == end_page) {
|
||||
uint8_t clip_mask =
|
||||
(uint8_t)((uint8_t)(0xFFu << start_bit) & (uint8_t)(0xFFu >> (7u - end_bit)));
|
||||
buf[(uint16_t)x + (uint16_t)start_page * DISPLAY_WIDTH] |= (uint8_t)(pattern & clip_mask);
|
||||
return;
|
||||
}
|
||||
|
||||
buf[(uint16_t)x + (uint16_t)start_page * DISPLAY_WIDTH] |=
|
||||
(uint8_t)(pattern & (uint8_t)(0xFFu << start_bit));
|
||||
|
||||
for(uint8_t page = (uint8_t)(start_page + 1); page < end_page; page++) {
|
||||
buf[(uint16_t)x + (uint16_t)page * DISPLAY_WIDTH] |= pattern;
|
||||
}
|
||||
|
||||
buf[(uint16_t)x + (uint16_t)end_page * DISPLAY_WIDTH] |=
|
||||
(uint8_t)(pattern & (uint8_t)(0xFFu >> (7u - end_bit)));
|
||||
}
|
||||
|
||||
static inline uint8_t get_page_mask(uint8_t page, uint8_t total_pages, uint8_t height) {
|
||||
if((page + 1u) != total_pages) return 0xFFu;
|
||||
uint8_t tail_bits = (uint8_t)(height & 7u);
|
||||
return tail_bits ? (uint8_t)((1u << tail_bits) - 1u) : 0xFFu;
|
||||
}
|
||||
|
||||
void Platform::DrawBitmap(int16_t x, int16_t y, const uint8_t* bmp) {
|
||||
if(!g_state || !bmp) return;
|
||||
|
||||
uint8_t w = bmp[0];
|
||||
uint8_t h = bmp[1];
|
||||
if(!w || !h) return;
|
||||
|
||||
int16_t x0 = x < 0 ? 0 : x;
|
||||
int16_t x1 = x + w;
|
||||
if(x1 > DISPLAY_WIDTH) x1 = DISPLAY_WIDTH;
|
||||
if(x0 >= x1) return;
|
||||
|
||||
const uint8_t* data = bmp + 2;
|
||||
uint8_t pages = (uint8_t)((h + 7u) >> 3);
|
||||
uint8_t* dst = g_state->back_buffer;
|
||||
|
||||
for(int16_t dx = x0; dx < x1; dx++) {
|
||||
uint8_t sx = (uint8_t)(dx - x);
|
||||
const uint8_t* src_col = data + sx;
|
||||
|
||||
for(uint8_t page = 0; page < pages; page++) {
|
||||
int16_t base_y = y + ((int16_t)page << 3);
|
||||
if(base_y <= -8 || base_y >= DISPLAY_HEIGHT) continue;
|
||||
|
||||
uint8_t src = src_col[(uint16_t)page * w] & get_page_mask(page, pages, h);
|
||||
if(src == 0) continue;
|
||||
|
||||
int16_t dst_page = floor_div8(base_y);
|
||||
uint8_t y_shift = (uint8_t)(base_y - (dst_page << 3));
|
||||
|
||||
uint8_t low = (uint8_t)(src << y_shift);
|
||||
if((uint16_t)dst_page < kDisplayPages) {
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)dst_page * DISPLAY_WIDTH;
|
||||
dst[idx] &= (uint8_t)~low;
|
||||
}
|
||||
|
||||
if(y_shift && (uint16_t)(dst_page + 1) < kDisplayPages) {
|
||||
uint8_t high = (uint8_t)(src >> (8u - y_shift));
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)(dst_page + 1) * DISPLAY_WIDTH;
|
||||
dst[idx] &= (uint8_t)~high;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Platform::DrawSolidBitmap(int16_t x, int16_t y, const uint8_t* bmp) {
|
||||
if(!g_state || !bmp) return;
|
||||
|
||||
uint8_t w = bmp[0];
|
||||
uint8_t h = bmp[1];
|
||||
if(!w || !h) return;
|
||||
|
||||
int16_t x0 = x < 0 ? 0 : x;
|
||||
int16_t x1 = x + w;
|
||||
if(x1 > DISPLAY_WIDTH) x1 = DISPLAY_WIDTH;
|
||||
if(x0 >= x1) return;
|
||||
|
||||
const uint8_t* data = bmp + 2;
|
||||
uint8_t pages = (uint8_t)((h + 7u) >> 3);
|
||||
uint8_t* dst = g_state->back_buffer;
|
||||
|
||||
for(int16_t dx = x0; dx < x1; dx++) {
|
||||
uint8_t sx = (uint8_t)(dx - x);
|
||||
const uint8_t* src_col = data + sx;
|
||||
|
||||
for(uint8_t page = 0; page < pages; page++) {
|
||||
int16_t base_y = y + ((int16_t)page << 3);
|
||||
if(base_y <= -8 || base_y >= DISPLAY_HEIGHT) continue;
|
||||
|
||||
uint8_t page_mask = get_page_mask(page, pages, h);
|
||||
uint8_t src = src_col[(uint16_t)page * w] & page_mask;
|
||||
uint8_t fill = (uint8_t)(~src) & page_mask;
|
||||
|
||||
int16_t dst_page = floor_div8(base_y);
|
||||
uint8_t y_shift = (uint8_t)(base_y - (dst_page << 3));
|
||||
|
||||
uint8_t region_low = (uint8_t)(page_mask << y_shift);
|
||||
uint8_t fill_low = (uint8_t)(fill << y_shift);
|
||||
if((uint16_t)dst_page < kDisplayPages) {
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)dst_page * DISPLAY_WIDTH;
|
||||
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_low) | fill_low);
|
||||
}
|
||||
|
||||
if(y_shift && (uint16_t)(dst_page + 1) < kDisplayPages) {
|
||||
uint8_t region_high = (uint8_t)(page_mask >> (8u - y_shift));
|
||||
uint8_t fill_high = (uint8_t)(fill >> (8u - y_shift));
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)(dst_page + 1) * DISPLAY_WIDTH;
|
||||
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_high) | fill_high);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Platform::DrawSprite(int16_t x, int16_t y, const uint8_t* bmp, uint8_t frame) {
|
||||
if(!g_state || !bmp) return;
|
||||
|
||||
uint8_t w = bmp[0];
|
||||
uint8_t h = bmp[1];
|
||||
if(!w || !h) return;
|
||||
|
||||
int16_t x0 = x < 0 ? 0 : x;
|
||||
int16_t x1 = x + w;
|
||||
if(x1 > DISPLAY_WIDTH) x1 = DISPLAY_WIDTH;
|
||||
if(x0 >= x1) return;
|
||||
|
||||
uint8_t pages = (uint8_t)((h + 7u) >> 3);
|
||||
uint16_t frame_size = (uint16_t)(w * pages);
|
||||
const uint8_t* data = bmp + 2 + (uint32_t)frame * frame_size * 2u;
|
||||
uint8_t* dst = g_state->back_buffer;
|
||||
|
||||
for(int16_t dx = x0; dx < x1; dx++) {
|
||||
uint8_t sx = (uint8_t)(dx - x);
|
||||
|
||||
for(uint8_t page = 0; page < pages; page++) {
|
||||
int16_t base_y = y + ((int16_t)page << 3);
|
||||
if(base_y <= -8 || base_y >= DISPLAY_HEIGHT) continue;
|
||||
|
||||
uint16_t src_index = (uint16_t)((page * w + sx) * 2u);
|
||||
uint8_t src = data[src_index];
|
||||
uint8_t mask = data[src_index + 1] & get_page_mask(page, pages, h);
|
||||
if(mask == 0) continue;
|
||||
|
||||
uint8_t fill = (uint8_t)(src & mask);
|
||||
int16_t dst_page = floor_div8(base_y);
|
||||
uint8_t y_shift = (uint8_t)(base_y - (dst_page << 3));
|
||||
|
||||
uint8_t region_low = (uint8_t)(mask << y_shift);
|
||||
uint8_t fill_low = (uint8_t)(fill << y_shift);
|
||||
if((uint16_t)dst_page < kDisplayPages) {
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)dst_page * DISPLAY_WIDTH;
|
||||
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_low) | fill_low);
|
||||
}
|
||||
|
||||
if(y_shift && (uint16_t)(dst_page + 1) < kDisplayPages) {
|
||||
uint8_t region_high = (uint8_t)(mask >> (8u - y_shift));
|
||||
uint8_t fill_high = (uint8_t)(fill >> (8u - y_shift));
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)(dst_page + 1) * DISPLAY_WIDTH;
|
||||
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_high) | fill_high);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class Platform
|
||||
{
|
||||
public:
|
||||
static uint8_t GetInput(void);
|
||||
static uint8_t* GetScreenBuffer();
|
||||
|
||||
static void PlaySound(const uint16_t* audioPattern);
|
||||
static bool IsAudioEnabled();
|
||||
static void SetAudioEnabled(bool isEnabled);
|
||||
|
||||
static void FillScreen(uint8_t col);
|
||||
static void PutPixel(uint8_t x, uint8_t y, uint8_t colour);
|
||||
static void DrawBitmap(int16_t x, int16_t y, const uint8_t *bitmap);
|
||||
static void DrawSolidBitmap(int16_t x, int16_t y, const uint8_t *bitmap);
|
||||
static void DrawSprite(int16_t x, int16_t y, const uint8_t *bitmap, const uint8_t *mask, uint8_t frame, uint8_t mask_frame);
|
||||
static void DrawSprite(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame);
|
||||
|
||||
static void DrawVLine(uint8_t x, int8_t y1, int8_t y2, uint8_t pattern);
|
||||
static void DrawBackground();
|
||||
};
|
||||
@@ -0,0 +1,328 @@
|
||||
#include "game/Player.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Projectile.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/Enemy.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/Sounds.h"
|
||||
#include "game/Particle.h"
|
||||
|
||||
#define USE_ROTATE_BOB 0
|
||||
#define STRAFE_TILT 14
|
||||
#define ROTATE_TILT 3
|
||||
|
||||
const char SignMessage1[] PROGMEM = "Abandon all hope ye who enter!";
|
||||
|
||||
void Player::Init()
|
||||
{
|
||||
NextLevel();
|
||||
hp = maxHP;
|
||||
}
|
||||
|
||||
void Player::NextLevel()
|
||||
{
|
||||
x = CELL_SIZE * 1 + CELL_SIZE / 2;
|
||||
y = CELL_SIZE * 1 + CELL_SIZE / 2;
|
||||
angle = FIXED_ANGLE_45;
|
||||
mana = maxMana;
|
||||
damageTime = 0;
|
||||
shakeTime = 0;
|
||||
reloadTime = 0;
|
||||
velocityX = 0;
|
||||
velocityY = 0;
|
||||
angularVelocity = 0;
|
||||
}
|
||||
|
||||
void Player::Fire()
|
||||
{
|
||||
if (mana >= manaFireCost)
|
||||
{
|
||||
reloadTime = 8;
|
||||
shakeTime = 6;
|
||||
|
||||
int16_t projectileX = x + FixedCos(angle + FIXED_ANGLE_90 / 2) / 4;
|
||||
int16_t projectileY = y + FixedSin(angle + FIXED_ANGLE_90 / 2) / 4;
|
||||
|
||||
ProjectileManager::FireProjectile(this, projectileX, projectileY, angle);
|
||||
mana -= manaFireCost;
|
||||
Platform::PlaySound(Sounds::Attack);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::Tick()
|
||||
{
|
||||
uint8_t input = Platform::GetInput();
|
||||
int8_t turnDelta = 0;
|
||||
int8_t targetTilt = 0;
|
||||
int8_t moveDelta = 0;
|
||||
int8_t strafeDelta = 0;
|
||||
|
||||
// Doom-style circle strafe: holding OK (fire) makes left/right strafe
|
||||
if (input & (INPUT_A | INPUT_B))
|
||||
{
|
||||
if (input & INPUT_LEFT)
|
||||
{
|
||||
strafeDelta--;
|
||||
}
|
||||
if (input & INPUT_RIGHT)
|
||||
{
|
||||
strafeDelta++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (input & INPUT_LEFT)
|
||||
{
|
||||
turnDelta -= TURN_SPEED * 2;
|
||||
}
|
||||
if (input & INPUT_RIGHT)
|
||||
{
|
||||
turnDelta += TURN_SPEED * 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Testing shooting / recoil mechanic
|
||||
|
||||
if (reloadTime > 0)
|
||||
{
|
||||
reloadTime--;
|
||||
}
|
||||
else if (input & INPUT_B)
|
||||
{
|
||||
Fire();
|
||||
}
|
||||
|
||||
|
||||
if (angularVelocity < turnDelta)
|
||||
{
|
||||
angularVelocity++;
|
||||
}
|
||||
else if (angularVelocity > turnDelta)
|
||||
{
|
||||
angularVelocity--;
|
||||
}
|
||||
|
||||
angle += angularVelocity >> 1;
|
||||
|
||||
if (input & INPUT_UP)
|
||||
{
|
||||
moveDelta++;
|
||||
}
|
||||
if (input & INPUT_DOWN)
|
||||
{
|
||||
moveDelta--;
|
||||
}
|
||||
|
||||
static int tiltTimer = 0;
|
||||
tiltTimer++;
|
||||
if (moveDelta && USE_ROTATE_BOB)
|
||||
{
|
||||
targetTilt = (int8_t)(FixedSin(tiltTimer * 10) / 32);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetTilt = 0;
|
||||
}
|
||||
|
||||
targetTilt += angularVelocity * ROTATE_TILT;
|
||||
targetTilt += strafeDelta * STRAFE_TILT;
|
||||
int8_t targetBob = moveDelta || strafeDelta ? FixedSin(tiltTimer * 10) / 128 : 0;
|
||||
|
||||
if (shakeTime > 0)
|
||||
{
|
||||
shakeTime--;
|
||||
targetBob += (Random() & 3) - 1;
|
||||
targetTilt += (Random() & 31) - 16;
|
||||
}
|
||||
|
||||
constexpr int tiltRate = 6;
|
||||
|
||||
if (Renderer::camera.tilt < targetTilt)
|
||||
{
|
||||
Renderer::camera.tilt += tiltRate;
|
||||
if (Renderer::camera.tilt > targetTilt)
|
||||
{
|
||||
Renderer::camera.tilt = targetTilt;
|
||||
}
|
||||
}
|
||||
else if (Renderer::camera.tilt > targetTilt)
|
||||
{
|
||||
Renderer::camera.tilt -= tiltRate;
|
||||
if (Renderer::camera.tilt < targetTilt)
|
||||
{
|
||||
Renderer::camera.tilt = targetTilt;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr int bobRate = 3;
|
||||
|
||||
if (Renderer::camera.bob < targetBob)
|
||||
{
|
||||
Renderer::camera.bob += bobRate;
|
||||
if (Renderer::camera.bob > targetBob)
|
||||
{
|
||||
Renderer::camera.bob = targetBob;
|
||||
}
|
||||
}
|
||||
else if (Renderer::camera.bob > targetBob)
|
||||
{
|
||||
Renderer::camera.bob -= bobRate;
|
||||
if (Renderer::camera.bob < targetBob)
|
||||
{
|
||||
Renderer::camera.bob = targetBob;
|
||||
}
|
||||
}
|
||||
|
||||
int16_t cosAngle = FixedCos(angle);
|
||||
int16_t sinAngle = FixedSin(angle);
|
||||
|
||||
int16_t cos90Angle = FixedCos(angle + FIXED_ANGLE_90);
|
||||
int16_t sin90Angle = FixedSin(angle + FIXED_ANGLE_90);
|
||||
//camera.x += (moveDelta * cosAngle) >> 4;
|
||||
//camera.y += (moveDelta * sinAngle) >> 4;
|
||||
velocityX += (moveDelta * cosAngle) / 24;
|
||||
velocityY += (moveDelta * sinAngle) / 24;
|
||||
|
||||
velocityX += (strafeDelta * cos90Angle) / 24;
|
||||
velocityY += (strafeDelta * sin90Angle) / 24;
|
||||
|
||||
Move(velocityX / 4, velocityY / 4);
|
||||
|
||||
velocityX = (velocityX * 7) / 8;
|
||||
velocityY = (velocityY * 7) / 8;
|
||||
|
||||
if (mana < maxMana && reloadTime == 0)
|
||||
{
|
||||
mana += manaRechargeRate;
|
||||
}
|
||||
|
||||
if (damageTime > 0)
|
||||
damageTime--;
|
||||
|
||||
uint8_t cellX = x / CELL_SIZE;
|
||||
uint8_t cellY = y / CELL_SIZE;
|
||||
|
||||
switch (Map::GetCellSafe(cellX, cellY))
|
||||
{
|
||||
case CellType::Potion:
|
||||
if (hp < maxHP)
|
||||
{
|
||||
if (hp + potionStrength > maxHP)
|
||||
hp = maxHP;
|
||||
else
|
||||
hp += potionStrength;
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Picked up a stimpack"));
|
||||
}
|
||||
break;
|
||||
case CellType::Coins:
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Picked up a health bonus"));
|
||||
Game::stats.coinsCollected++;
|
||||
break;
|
||||
case CellType::Crown:
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Picked up some armor"));
|
||||
Game::stats.crownsCollected++;
|
||||
break;
|
||||
case CellType::Scroll:
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Picked up an armor bonus"));
|
||||
Game::stats.scrollsCollected++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool Player::IsWorldColliding() const
|
||||
{
|
||||
return Map::IsBlockedAtWorldPosition(x - collisionSize, y - collisionSize)
|
||||
|| Map::IsBlockedAtWorldPosition(x + collisionSize, y - collisionSize)
|
||||
|| Map::IsBlockedAtWorldPosition(x + collisionSize, y + collisionSize)
|
||||
|| Map::IsBlockedAtWorldPosition(x - collisionSize, y + collisionSize);
|
||||
}
|
||||
|
||||
bool Player::CheckCollisions()
|
||||
{
|
||||
int16_t lookAheadX = (x + (FixedCos(angle) * lookAheadDistance) / FIXED_ONE);
|
||||
int16_t lookAheadY = (y + (FixedSin(angle) * lookAheadDistance) / FIXED_ONE);
|
||||
uint8_t lookAheadCellX = (uint8_t)(lookAheadX / CELL_SIZE);
|
||||
uint8_t lookAheadCellY = (uint8_t)(lookAheadY / CELL_SIZE);
|
||||
|
||||
CellType lookAheadCell = Map::GetCellSafe(lookAheadCellX, lookAheadCellY);
|
||||
switch (lookAheadCell)
|
||||
{
|
||||
case CellType::Chest:
|
||||
Map::SetCell(lookAheadCellX, lookAheadCellY, CellType::ChestOpened);
|
||||
ParticleSystemManager::CreateExplosion(lookAheadX, lookAheadY, true);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Found a backpack of ammo!"));
|
||||
Game::stats.chestsOpened++;
|
||||
break;
|
||||
case CellType::Sign:
|
||||
Game::ShowMessage(SignMessage1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if (IsWorldColliding())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (EnemyManager::GetOverlappingEnemy(*this))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Player::Move(int16_t deltaX, int16_t deltaY)
|
||||
{
|
||||
x += deltaX;
|
||||
y += deltaY;
|
||||
|
||||
if (CheckCollisions())
|
||||
{
|
||||
y -= deltaY;
|
||||
if (CheckCollisions())
|
||||
{
|
||||
x -= deltaX;
|
||||
y += deltaY;
|
||||
|
||||
if (CheckCollisions())
|
||||
{
|
||||
y -= deltaY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::Damage(uint8_t damageAmount)
|
||||
{
|
||||
if(shakeTime < 6)
|
||||
shakeTime = 6;
|
||||
|
||||
damageTime = 8;
|
||||
|
||||
if (hp <= damageAmount)
|
||||
{
|
||||
Platform::PlaySound(Sounds::PlayerDeath);
|
||||
hp = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform::PlaySound(Sounds::Ouch);
|
||||
hp -= damageAmount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Entity.h"
|
||||
|
||||
class Player : public Entity
|
||||
{
|
||||
public:
|
||||
uint8_t angle;
|
||||
int16_t velocityX, velocityY;
|
||||
int8_t angularVelocity;
|
||||
|
||||
uint8_t shakeTime;
|
||||
uint8_t damageTime;
|
||||
uint8_t reloadTime;
|
||||
|
||||
static constexpr uint8_t maxHP = 100;
|
||||
static constexpr uint8_t maxMana = 100;
|
||||
static constexpr uint8_t manaFireCost = 20;
|
||||
static constexpr uint8_t manaRechargeRate = 1;
|
||||
static constexpr uint8_t attackStrength = 10;
|
||||
static constexpr uint8_t collisionSize = 48;
|
||||
static constexpr uint8_t lookAheadDistance = 60;
|
||||
static constexpr uint8_t potionStrength = 25;
|
||||
|
||||
uint8_t hp;
|
||||
uint8_t mana;
|
||||
|
||||
void Init();
|
||||
void NextLevel();
|
||||
void Tick();
|
||||
void Fire();
|
||||
void Move(int16_t deltaX, int16_t deltaY);
|
||||
bool CheckCollisions();
|
||||
void Damage(uint8_t amount);
|
||||
bool IsWorldColliding() const;
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
#include "game/Defines.h"
|
||||
#include "game/Projectile.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Particle.h"
|
||||
#include "game/Enemy.h"
|
||||
#include "game/Generated/SpriteTypes.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Sounds.h"
|
||||
|
||||
Projectile ProjectileManager::projectiles[ProjectileManager::MAX_PROJECTILES];
|
||||
|
||||
Projectile* ProjectileManager::FireProjectile(Entity* owner, int16_t x, int16_t y, uint8_t angle)
|
||||
{
|
||||
for (Projectile& p : projectiles)
|
||||
{
|
||||
if(p.life == 0)
|
||||
{
|
||||
if (owner == &Game::player)
|
||||
p.ownerId = Projectile::playerOwnerId;
|
||||
else
|
||||
{
|
||||
for (uint8_t n = 0; n < EnemyManager::maxEnemies; n++)
|
||||
{
|
||||
if (&EnemyManager::enemies[n] == owner)
|
||||
{
|
||||
p.ownerId = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.life = 255;
|
||||
p.x = x;
|
||||
p.y = y;
|
||||
p.angle = angle;
|
||||
return &p;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Entity* Projectile::GetOwner() const
|
||||
{
|
||||
if (ownerId == playerOwnerId)
|
||||
return &Game::player;
|
||||
return &EnemyManager::enemies[ownerId];
|
||||
}
|
||||
|
||||
void ProjectileManager::Update()
|
||||
{
|
||||
for (Projectile& p : projectiles)
|
||||
{
|
||||
if(p.life > 0)
|
||||
{
|
||||
p.life--;
|
||||
|
||||
int16_t deltaX = FixedCos(p.angle) / 4;
|
||||
int16_t deltaY = FixedSin(p.angle) / 4;
|
||||
|
||||
p.x += deltaX;
|
||||
p.y += deltaY;
|
||||
|
||||
bool hitAnything = false;
|
||||
|
||||
Entity* owner = p.GetOwner();
|
||||
|
||||
if (Map::IsBlockedAtWorldPosition(p.x, p.y))
|
||||
{
|
||||
uint8_t cellX = p.x / CELL_SIZE;
|
||||
uint8_t cellY = p.y / CELL_SIZE;
|
||||
|
||||
if (Map::GetCellSafe(cellX, cellY) == CellType::Urn)
|
||||
{
|
||||
// Exploding barrel: splash damage to everything nearby
|
||||
int16_t barrelX = cellX * CELL_SIZE + CELL_SIZE / 2;
|
||||
int16_t barrelY = cellY * CELL_SIZE + CELL_SIZE / 2;
|
||||
constexpr int16_t blastRadius = CELL_SIZE + CELL_SIZE / 2;
|
||||
constexpr uint8_t enemyBlastDamage = 40;
|
||||
constexpr uint8_t playerBlastDamage = 15;
|
||||
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
ParticleSystemManager::CreateExplosion(barrelX, barrelY, true);
|
||||
|
||||
for (uint8_t n = 0; n < EnemyManager::maxEnemies; n++)
|
||||
{
|
||||
Enemy& enemy = EnemyManager::enemies[n];
|
||||
if (!enemy.IsValid())
|
||||
continue;
|
||||
int16_t dx = enemy.x - barrelX;
|
||||
int16_t dy = enemy.y - barrelY;
|
||||
if (ABS(dx) < blastRadius && ABS(dy) < blastRadius)
|
||||
{
|
||||
enemy.Damage(enemyBlastDamage);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
int16_t dx = Game::player.x - barrelX;
|
||||
int16_t dy = Game::player.y - barrelY;
|
||||
if (ABS(dx) < blastRadius && ABS(dy) < blastRadius)
|
||||
{
|
||||
// barrels hurt but never kill the player outright
|
||||
uint8_t dmg = playerBlastDamage;
|
||||
if (Game::player.hp <= dmg)
|
||||
dmg = Game::player.hp > 1 ? (uint8_t)(Game::player.hp - 1) : 0;
|
||||
if (dmg)
|
||||
Game::player.Damage(dmg);
|
||||
}
|
||||
}
|
||||
|
||||
// occasionally the barrel leaves a pickup behind
|
||||
switch ((Random() % 6))
|
||||
{
|
||||
case 0:
|
||||
Map::SetCell(cellX, cellY, CellType::Potion);
|
||||
break;
|
||||
case 1:
|
||||
Map::SetCell(cellX, cellY, CellType::Coins);
|
||||
break;
|
||||
}
|
||||
Platform::PlaySound(Sounds::Kill);
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform::PlaySound(Sounds::Hit);
|
||||
}
|
||||
|
||||
hitAnything = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (owner == &Game::player)
|
||||
{
|
||||
Enemy* overlappingEnemy = EnemyManager::GetOverlappingEnemy(p.x, p.y);
|
||||
if (overlappingEnemy)
|
||||
{
|
||||
overlappingEnemy->Damage(Player::attackStrength);
|
||||
|
||||
hitAnything = true;
|
||||
}
|
||||
}
|
||||
else if(Game::player.IsOverlappingPoint(p.x, p.y))
|
||||
{
|
||||
const EnemyArchetype* enemyArchetype = ((Enemy*)owner)->GetArchetype();
|
||||
if (enemyArchetype)
|
||||
{
|
||||
Game::player.Damage(enemyArchetype->GetAttackStrength());
|
||||
if (Game::player.hp == 0)
|
||||
{
|
||||
Game::stats.killedBy = ((Enemy*)owner)->GetType();
|
||||
}
|
||||
}
|
||||
hitAnything = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hitAnything)
|
||||
{
|
||||
ParticleSystemManager::CreateExplosion(p.x - deltaX, p.y - deltaY);
|
||||
p.life = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectileManager::Init()
|
||||
{
|
||||
for (Projectile& p : projectiles)
|
||||
{
|
||||
p.life = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectileManager::Draw()
|
||||
{
|
||||
for(Projectile& p : projectiles)
|
||||
{
|
||||
if (p.life > 0)
|
||||
{
|
||||
Renderer::DrawObject(p.ownerId == Projectile::playerOwnerId ? projectileSpriteData : enemyProjectileSpriteData, p.x, p.y, 32, AnchorType::BelowCenter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Entity.h"
|
||||
|
||||
class Projectile : public Entity
|
||||
{
|
||||
public:
|
||||
uint8_t angle;
|
||||
uint8_t life;
|
||||
uint8_t ownerId;
|
||||
|
||||
static constexpr uint8_t playerOwnerId = 0xff;
|
||||
|
||||
Entity* GetOwner() const;
|
||||
};
|
||||
|
||||
class ProjectileManager
|
||||
{
|
||||
public:
|
||||
static constexpr int MAX_PROJECTILES = 8;
|
||||
static Projectile projectiles[MAX_PROJECTILES];
|
||||
|
||||
static Projectile* FireProjectile(Entity* owner, int16_t x, int16_t y, uint8_t angle);
|
||||
static void Init();
|
||||
static void Draw();
|
||||
static void Update();
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "game/Sounds.h"
|
||||
|
||||
// Shotgun blast: sharp crack followed by a fast descending boom with a
|
||||
// short pump echo. All frequencies within the buzzer's 100-2500 Hz range.
|
||||
const uint16_t Sounds::Attack[] PROGMEM = {
|
||||
900, 2, 500, 3, 320, 4, 230, 5,
|
||||
180, 6, 150, 7, 128, 9, 112, 11, 104, 13, 100, 15,
|
||||
0, 8,
|
||||
170, 4, 135, 6, 112, 9, 100, 14,
|
||||
TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Kill[] PROGMEM = {
|
||||
0x0151,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x014b,0x0007,0x0136,0x0007,0x0146,0x0007,0x0169,0x0007,0x019e,0x0007,0x007f,0x0007,0x0185,
|
||||
0x0007,0x0228,0x0007,0x026d,0x0007,0x02fc,0x0007,0x02e0,0x0007,0x01fd,0x0007,0x0219,0x0007,0x033c,0x0007,0x00e7,0x0007,0x0281,0x0007,0x026d,
|
||||
0x000e,0x052d,0x000e,0x04da,0x000e,0x011c,0x0007,0x0387,0x0007,0x0360,0x0007,0x033c,0x0007,0x0387,0x0007,0x02ad,0x000e,0x02c6,0x0007,0x010c,
|
||||
0x000e,0x02e0,0x0007,0x02c6,0x0007,0x0296,0x000e,0x0281,0x0007,0x02ad,0x0007,0x02c6,0x0007,0x02e0,0x0007,0x00e1,0x0007,0x031b,0x0007,0x0248,
|
||||
0x0007,0x0219,0x0007,0x01f1,0x0007,0x01d9,0x000e,0x01f1,0x0007,0x020b,0x0007,0x02ad,0x0007,0x00d1,0x0007,0x0248,0x0007,0x0238,0x0007,0x0219,
|
||||
0x0007,0x0228,0x000e,0x020b,0x0007,0x01e5,0x0007,0x01d9,0x0007, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Hit[] PROGMEM = {
|
||||
0x0195,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x0416,0x0007,0x007f,0x0007,0x0000,0x000e,0x0177,0x0007,0x0000,0x001d,0x0146,0x0007,0x0140,
|
||||
0x0007,0x013b,0x0007,0x0000,0x0047,0x00e7,0x0007,0x00e4,0x0007,0x0000,0x0015,0x009f,0x0007,0x009d,0x0007,0x0000,0x0088,0x0088,0x0007, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::PlayerDeath[] PROGMEM = {
|
||||
0x01b0,0x0007,0x0000,0x0015,0x00c8,0x0007,0x0000,0x0007,0x03b2,0x0007,0x0360,0x0007,0x02e0,0x0007,0x0296,0x0007,0x0248,0x0007,0x0219,0x0007,0x01fd,
|
||||
0x0007,0x0450,0x0007,0x01ce,0x0007,0x01b9,0x0007,0x01a7,0x0007,0x0450,0x0007,0x017e,0x0007,0x0163,0x0007,0x0140,0x0007,0x052d,0x0007,0x0110,
|
||||
0x0007,0x0109,0x0007,0x0102,0x0007,0x00f8,0x0007,0x04da,0x0007,0x00ca,0x0007,0x00bb,0x0007,0x00b3,0x0007,0x0491,0x0007,0x0096,0x0007,0x00d8,
|
||||
0x0007,0x0000,0x001d,0x0296,0x0007,0x0000,0x002b,0x0228,0x0007,0x0000,0x0024,0x00fb,0x000e, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::SpotPlayer[] PROGMEM = {
|
||||
0x0110,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x01d9,0x0007,0x01fd,0x0007,0x020b,0x000e,0x0228,0x000e,0x0238,0x0007,0x0248,0x0007,0x026d,
|
||||
0x0007,0x0281,0x0032,0x026d,0x0015,0x0248,0x0007,0x0238,0x0007,0x01b0,0x0007,0x017e,0x0007,0x0169,0x0007,0x014b,0x0007,0x0136,0x0007,0x0131,
|
||||
0x0007,0x0102,0x0007,0x00f8,0x0007,0x00f2,0x0007,0x00e9,0x0007,0x00e4,0x0007,0x00d8,0x0007,0x00bd,0x0007,0x00ab,0x0007,0x009d,0x0007,0x0096,
|
||||
0x0007,0x0094,0x0007,0x0092,0x0007,0x008e,0x0007,0x008b,0x0007,0x008a,0x0007,0x0089,0x0007,0x0088,0x0007,0x0087,0x0007,0x0086,0x0007,0x0085,
|
||||
0x0007,0x0084,0x0007,0x0083,0x0007,0x0082,0x0007,0x0081,0x000e,0x0081,0x0007,0x0080,0x0007,0x007e,0x0007,0x007d,0x0007,0x007d,0x0007,0x0000,
|
||||
0x0040,0x0090,0x0032, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Shoot[] PROGMEM = {
|
||||
0x02e0,0x0007,0x0000,0x0015,0x03e2,0x0007,0x0000,0x0007,0x04da,0x0015,0x00b4,0x0007,0x01d9,0x0007,0x0000,0x0007,0x01f1,0x000e,0x0080,0x0007,0x01f1,
|
||||
0x0007,0x0000,0x000e,0x025a,0x0007,0x00e4,0x0007,0x0000,0x0015,0x00c1,0x0007,0x0000,0x000e,0x01e5,0x0007,0x0000,0x0007,0x00ac,0x0007,0x0000,
|
||||
0x0015,0x0091,0x0007, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Pickup[] PROGMEM = {
|
||||
0x0120,0x0007,0x0000,0x0015,0x00e9,0x0007,0x0000,0x0007,0x0156,0x0015,0x0000,0x0032,0x0185,0x001d,0x0000,0x0040,0x020b,0x0015,0x0000,0x0032,0x0387,
|
||||
0x0024,0x0000,0x0040,0x0387,0x002b,0x0000,0x0040,0x03b2,0x0032, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Ouch[] PROGMEM = {
|
||||
0x01ce,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x0416,0x0007,0x0491,0x0007,0x03b2,0x0007,0x04da,0x0007,0x052d,0x0015,0x04da,0x0007,0x0491,
|
||||
0x0007,0x02e0,0x0007,0x0296,0x0007,0x025a,0x0007,0x01f1,0x0007,0x0219,0x0007,0x0000,0x0007,0x01ce,0x0007,0x007c,0x000e,0x015c,0x0007,0x007d,
|
||||
0x000e,0x0120,0x0007,0x007d,0x000e,0x00ef,0x0007,0x00d5,0x0007,0x00ca,0x0007,0x0000,0x0007,0x00c1,0x0007,0x00b9,0x0007,0x00b3,0x0007,0x00a9,
|
||||
0x0007,0x007d,0x0007,0x007e,0x0007,0x0093,0x0007,0x007e,0x0007,0x0000,0x0007,0x0089,0x0007,0x0085,0x0007,0x0082,0x0007,0x0080,0x0007,0x007e,
|
||||
0x0007,0x007d,0x0007, TONES_END
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include "game/Defines.h"
|
||||
|
||||
#define TONES_END 0x8000
|
||||
|
||||
class Sounds
|
||||
{
|
||||
public:
|
||||
static const uint16_t Attack[];
|
||||
static const uint16_t Kill[];
|
||||
static const uint16_t Hit[];
|
||||
static const uint16_t PlayerDeath[];
|
||||
static const uint16_t SpotPlayer[];
|
||||
static const uint16_t Shoot[];
|
||||
static const uint16_t Pickup[];
|
||||
static const uint16_t Ouch[];
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "game/Defines.h"
|
||||
|
||||
// Tech-base wall panel: top/bottom seams with vertical panel gaps
|
||||
const uint8_t vectorTexture0[] PROGMEM =
|
||||
{
|
||||
6,
|
||||
0, 18, 128, 18,
|
||||
0, 110, 128, 110,
|
||||
32, 18, 32, 110,
|
||||
64, 18, 64, 110,
|
||||
96, 18, 96, 110,
|
||||
48, 64, 80, 64,
|
||||
};
|
||||
|
||||
const uint8_t vectorTexture1[] PROGMEM =
|
||||
{
|
||||
6,
|
||||
0, 16, 128, 16 ,
|
||||
0, 112, 128, 112 ,
|
||||
0, 16, 0, 112,
|
||||
0, 16, 128, 112,
|
||||
0, 112, 128, 16,
|
||||
128, 16, 128, 112,
|
||||
|
||||
/* 16, 16, 112, 16 ,
|
||||
16, 16, 16, 128,
|
||||
48, 16, 48, 128,
|
||||
80, 16, 80, 128,
|
||||
112, 16, 112, 128,*/
|
||||
};
|
||||
|
||||
const uint8_t vectorTexture2[] PROGMEM =
|
||||
{
|
||||
12,
|
||||
38,13,90,13,
|
||||
38,13,64,38,
|
||||
64,38,90,13,
|
||||
13,38,38,64,
|
||||
13,38,13,90,
|
||||
13,90,38,64,
|
||||
38,115,90,115,
|
||||
38,115,64,90,
|
||||
64,90,90,115,
|
||||
90,64,115,38,
|
||||
90,64,115,90,
|
||||
115,38,115,90,
|
||||
};
|
||||
|
||||
const uint8_t* const textures[] PROGMEM =
|
||||
{
|
||||
vectorTexture0,
|
||||
vectorTexture1,
|
||||
vectorTexture2,
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <furi.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
#define EEPROM_LIB_PATH APP_DATA_PATH("eeprom.bin")
|
||||
|
||||
class EEPROMClass {
|
||||
public:
|
||||
static constexpr int kSize = 16;
|
||||
static constexpr size_t kPathSize = 128;
|
||||
|
||||
EEPROMClass()
|
||||
: loaded_(false)
|
||||
, dirty_(false)
|
||||
, path_resolved_(false) {
|
||||
memset(mem_, 0x00, kSize);
|
||||
memset(file_path_, 0x00, kPathSize);
|
||||
strncpy(file_path_, EEPROM_LIB_PATH, kPathSize - 1);
|
||||
}
|
||||
|
||||
void begin() {
|
||||
ensureLoaded_();
|
||||
}
|
||||
|
||||
int length() const {
|
||||
return kSize;
|
||||
}
|
||||
|
||||
uint8_t read(int addr) const {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr >= kSize) return 0;
|
||||
return mem_[addr];
|
||||
}
|
||||
|
||||
void write(int addr, uint8_t value) {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr >= kSize) return;
|
||||
mem_[addr] = value;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void update(int addr, uint8_t value) {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr >= kSize) return;
|
||||
if(mem_[addr] != value) {
|
||||
mem_[addr] = value;
|
||||
dirty_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T& get(int addr, T& out) const {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr + (int)sizeof(T) > kSize) return out;
|
||||
memcpy(&out, mem_ + addr, sizeof(T));
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
const T& put(int addr, const T& in) {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr + (int)sizeof(T) > kSize) return in;
|
||||
|
||||
bool changed = false;
|
||||
const uint8_t* src = reinterpret_cast<const uint8_t*>(&in);
|
||||
for(size_t i = 0; i < sizeof(T); i++) {
|
||||
int a = addr + (int)i;
|
||||
if(mem_[a] != src[i]) {
|
||||
mem_[a] = src[i];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if(changed) dirty_ = true;
|
||||
return in;
|
||||
}
|
||||
|
||||
void clear(uint8_t value = 0) {
|
||||
ensureLoaded_();
|
||||
memset(mem_, value, kSize);
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
bool commit() {
|
||||
ensureLoaded_();
|
||||
if(!dirty_) return true;
|
||||
|
||||
const bool ok = writeFile_();
|
||||
if(ok) dirty_ = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool isDirty() const {
|
||||
return dirty_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool resolvePathIfNeeded_(Storage* storage) const {
|
||||
if(path_resolved_) return true;
|
||||
if(!storage) return false;
|
||||
|
||||
FuriString* path = furi_string_alloc_set_str(file_path_);
|
||||
if(!path) return false;
|
||||
|
||||
storage_common_resolve_path_and_ensure_app_directory(storage, path);
|
||||
const char* resolved = furi_string_get_cstr(path);
|
||||
|
||||
bool ok = false;
|
||||
if(resolved && resolved[0]) {
|
||||
const size_t len = strlen(resolved);
|
||||
if(len < kPathSize) {
|
||||
memcpy(file_path_, resolved, len + 1);
|
||||
path_resolved_ = true;
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_free(path);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void ensureDefaultDir_(Storage* storage) {
|
||||
if(!storage) return;
|
||||
(void)storage_common_mkdir(storage, STORAGE_APP_DATA_PATH_PREFIX);
|
||||
}
|
||||
|
||||
void ensureLoaded_() const {
|
||||
if(loaded_) return;
|
||||
|
||||
Storage* storage = (Storage*)furi_record_open(RECORD_STORAGE);
|
||||
if(!storage) {
|
||||
return;
|
||||
}
|
||||
|
||||
(void)resolvePathIfNeeded_(storage);
|
||||
ensureDefaultDir_(storage);
|
||||
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(!file) {
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(mem_, 0x00, kSize);
|
||||
bool ok = storage_file_open(file, file_path_, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS);
|
||||
if(ok) {
|
||||
const uint64_t file_size = storage_file_size(file);
|
||||
bool need_rewrite = false;
|
||||
|
||||
(void)storage_file_seek(file, 0, true);
|
||||
const size_t rd = storage_file_read(file, mem_, kSize);
|
||||
if(rd < (size_t)kSize) {
|
||||
need_rewrite = true;
|
||||
}
|
||||
|
||||
if(file_size != (uint64_t)kSize) {
|
||||
need_rewrite = true;
|
||||
}
|
||||
|
||||
if(need_rewrite) {
|
||||
(void)storage_file_seek(file, 0, true);
|
||||
const size_t wr = storage_file_write(file, mem_, kSize);
|
||||
if(wr == (size_t)kSize) {
|
||||
(void)storage_file_truncate(file);
|
||||
(void)storage_file_sync(file);
|
||||
}
|
||||
}
|
||||
(void)storage_file_close(file);
|
||||
} else {
|
||||
(void)storage_file_close(file);
|
||||
}
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
loaded_ = true;
|
||||
dirty_ = false;
|
||||
}
|
||||
|
||||
bool writeFile_() const {
|
||||
Storage* storage = (Storage*)furi_record_open(RECORD_STORAGE);
|
||||
if(!storage) return false;
|
||||
|
||||
if(!path_resolved_ && !resolvePathIfNeeded_(storage)) {
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
ensureDefaultDir_(storage);
|
||||
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(!file) {
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = storage_file_open(file, file_path_, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS);
|
||||
bool success = false;
|
||||
|
||||
if(ok) {
|
||||
(void)storage_file_seek(file, 0, true);
|
||||
size_t wr = storage_file_write(file, mem_, kSize);
|
||||
(void)storage_file_truncate(file);
|
||||
(void)storage_file_sync(file);
|
||||
(void)storage_file_close(file);
|
||||
success = (wr == (size_t)kSize);
|
||||
} else {
|
||||
(void)storage_file_close(file);
|
||||
}
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return success;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable uint8_t mem_[kSize];
|
||||
mutable bool loaded_;
|
||||
mutable bool dirty_;
|
||||
mutable bool path_resolved_;
|
||||
|
||||
mutable char file_path_[kPathSize];
|
||||
};
|
||||
|
||||
#if (__cplusplus >= 201703L)
|
||||
inline EEPROMClass EEPROM;
|
||||
#else
|
||||
extern EEPROMClass EEPROM;
|
||||
#ifdef EEPROM_DEFINE_INSTANCE
|
||||
EEPROMClass EEPROM;
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
//lib/flipper.h
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define DISPLAY_WIDTH 128
|
||||
#define DISPLAY_HEIGHT 64
|
||||
#define BUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_HEIGHT / 8)
|
||||
|
||||
typedef struct {
|
||||
uint8_t back_buffer[BUFFER_SIZE];
|
||||
uint8_t front_buffer[BUFFER_SIZE];
|
||||
|
||||
Gui* gui;
|
||||
Canvas* canvas;
|
||||
FuriMutex* fb_mutex;
|
||||
|
||||
volatile uint8_t input_state;
|
||||
volatile bool exit_requested;
|
||||
volatile bool audio_enabled;
|
||||
|
||||
// back-hold логика
|
||||
bool back_hold_active;
|
||||
uint16_t back_hold_start;
|
||||
bool back_hold_handled;
|
||||
|
||||
// input pubsub
|
||||
FuriPubSub* input_events;
|
||||
FuriPubSubSubscription* input_sub;
|
||||
} FlipperState;
|
||||
|
||||
extern FlipperState* g_state;
|
||||
@@ -0,0 +1,261 @@
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "lib/flipper.h"
|
||||
#include "lib/EEPROM.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/Platform.h"
|
||||
|
||||
#define TARGET_FRAMERATE 30
|
||||
#define HOLD_TIME_MS 300
|
||||
|
||||
FlipperState* g_state = NULL;
|
||||
|
||||
static volatile uint32_t s_input_cb_inflight = 0;
|
||||
static volatile uint32_t s_fb_cb_inflight = 0;
|
||||
static volatile uint8_t s_back_pressed = 0;
|
||||
|
||||
static inline void wait_inflight_zero(volatile uint32_t* counter) {
|
||||
while(__atomic_load_n(counter, __ATOMIC_ACQUIRE) != 0) {
|
||||
furi_delay_ms(1);
|
||||
}
|
||||
}
|
||||
|
||||
inline bool audio_enable(){
|
||||
return !furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode);
|
||||
}
|
||||
|
||||
static void framebuffer_commit_callback(
|
||||
uint8_t* data,
|
||||
size_t size,
|
||||
CanvasOrientation orientation,
|
||||
void* context) {
|
||||
__atomic_fetch_add(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
|
||||
FlipperState* state = (FlipperState*)context;
|
||||
if(!state || !data || size < BUFFER_SIZE) {
|
||||
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
return;
|
||||
}
|
||||
(void)orientation;
|
||||
|
||||
if(furi_mutex_acquire(state->fb_mutex, 0) != FuriStatusOk) {
|
||||
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* src = state->front_buffer;
|
||||
for(size_t i = 0; i < BUFFER_SIZE; i++) {
|
||||
data[i] = (uint8_t)(src[i] ^ 0xFF);
|
||||
}
|
||||
|
||||
furi_mutex_release(state->fb_mutex);
|
||||
|
||||
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
static void input_events_callback(const void* value, void* ctx) {
|
||||
if(!value || !ctx) return;
|
||||
|
||||
__atomic_fetch_add(&s_input_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
|
||||
FlipperState* state = (FlipperState*)ctx;
|
||||
const InputEvent* event = (const InputEvent*)value;
|
||||
|
||||
uint8_t bit = 0;
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
bit = INPUT_UP;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
bit = INPUT_DOWN;
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
bit = INPUT_LEFT;
|
||||
break;
|
||||
case InputKeyRight:
|
||||
bit = INPUT_RIGHT;
|
||||
break;
|
||||
case InputKeyOk:
|
||||
bit = INPUT_B;
|
||||
break;
|
||||
case InputKeyBack:
|
||||
if((event->type == InputTypePress) || (event->type == InputTypeRepeat)) {
|
||||
(void)__atomic_store_n(&s_back_pressed, 1, __ATOMIC_RELAXED);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
(void)__atomic_store_n(&s_back_pressed, 0, __ATOMIC_RELAXED);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(state && bit) {
|
||||
if((event->type == InputTypePress) || (event->type == InputTypeRepeat)) {
|
||||
(void)__atomic_fetch_or((uint8_t*)&state->input_state, bit, __ATOMIC_RELAXED);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
(void)__atomic_fetch_and(
|
||||
(uint8_t*)&state->input_state, (uint8_t)~bit, __ATOMIC_RELAXED);
|
||||
}
|
||||
}
|
||||
|
||||
__atomic_fetch_sub(&s_input_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
extern "C" int32_t flipdoom_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
Gui* gui = NULL;
|
||||
Canvas* canvas = NULL;
|
||||
FuriPubSub* input_events = NULL;
|
||||
FuriPubSubSubscription* input_sub = NULL;
|
||||
|
||||
FlipperState* st = (FlipperState*)malloc(sizeof(FlipperState));
|
||||
if(!st) return -1;
|
||||
memset(st, 0, sizeof(FlipperState));
|
||||
g_state = st;
|
||||
|
||||
do {
|
||||
st->fb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
if(!st->fb_mutex) break;
|
||||
|
||||
memset(st->back_buffer, 0x00, BUFFER_SIZE);
|
||||
memset(st->front_buffer, 0x00, BUFFER_SIZE);
|
||||
|
||||
EEPROM.begin();
|
||||
furi_delay_ms(50);
|
||||
Platform::SetAudioEnabled(audio_enable());
|
||||
Game::menu.ReadSave();
|
||||
|
||||
gui = (Gui*)furi_record_open(RECORD_GUI);
|
||||
if(!gui) break;
|
||||
st->gui = gui;
|
||||
|
||||
gui_add_framebuffer_callback(gui, framebuffer_commit_callback, st);
|
||||
|
||||
canvas = gui_direct_draw_acquire(gui);
|
||||
if(!canvas) break;
|
||||
st->canvas = canvas;
|
||||
|
||||
input_events = (FuriPubSub*)furi_record_open(RECORD_INPUT_EVENTS);
|
||||
if(!input_events) break;
|
||||
st->input_events = input_events;
|
||||
|
||||
input_sub = furi_pubsub_subscribe(input_events, input_events_callback, st);
|
||||
if(!input_sub) break;
|
||||
st->input_sub = input_sub;
|
||||
|
||||
const uint32_t tick_hz = furi_kernel_get_tick_frequency();
|
||||
uint32_t period_ticks = (tick_hz + (TARGET_FRAMERATE / 2)) / TARGET_FRAMERATE;
|
||||
if(period_ticks == 0) period_ticks = 1;
|
||||
const uint32_t hold_ticks = (uint32_t)((HOLD_TIME_MS * tick_hz + 999u) / 1000u);
|
||||
|
||||
uint32_t next_tick = furi_get_tick();
|
||||
|
||||
bool back_was_pressed = false;
|
||||
bool back_hold_fired = false;
|
||||
uint32_t back_press_tick = 0;
|
||||
|
||||
while(!st->exit_requested) {
|
||||
uint32_t now = furi_get_tick();
|
||||
|
||||
// frame pacing
|
||||
if((int32_t)(now - next_tick) < 0) {
|
||||
uint32_t dt_ticks = next_tick - now;
|
||||
uint32_t dt_ms = (dt_ticks * 1000u) / tick_hz;
|
||||
furi_delay_ms(dt_ms ? dt_ms : 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if((int32_t)(now - next_tick) > (int32_t)(period_ticks * 2)) {
|
||||
next_tick = now;
|
||||
}
|
||||
next_tick += period_ticks;
|
||||
|
||||
const bool back_pressed = (__atomic_load_n(&s_back_pressed, __ATOMIC_RELAXED) != 0);
|
||||
|
||||
// BACK hold logic
|
||||
if(!back_pressed) {
|
||||
back_was_pressed = false;
|
||||
back_hold_fired = false;
|
||||
} else {
|
||||
if(!back_was_pressed) {
|
||||
back_was_pressed = true;
|
||||
back_press_tick = now;
|
||||
back_hold_fired = false;
|
||||
}
|
||||
|
||||
if(!back_hold_fired && ((uint32_t)(now - back_press_tick) >= hold_ticks)) {
|
||||
back_hold_fired = true;
|
||||
if(Game::InMenu())
|
||||
st->exit_requested = true;
|
||||
else
|
||||
Game::GoToMenu();
|
||||
}
|
||||
}
|
||||
|
||||
if(st->exit_requested) break;
|
||||
|
||||
Game::Tick();
|
||||
Game::Draw();
|
||||
|
||||
// swap for framebuffer callback
|
||||
furi_mutex_acquire(st->fb_mutex, FuriWaitForever);
|
||||
memcpy(st->front_buffer, st->back_buffer, BUFFER_SIZE);
|
||||
furi_mutex_release(st->fb_mutex);
|
||||
|
||||
canvas_commit(canvas);
|
||||
}
|
||||
} while(false);
|
||||
|
||||
Game::menu.WriteSave();
|
||||
|
||||
if(input_sub && input_events) {
|
||||
furi_pubsub_unsubscribe(input_events, input_sub);
|
||||
input_sub = NULL;
|
||||
}
|
||||
st->input_sub = NULL;
|
||||
|
||||
wait_inflight_zero(&s_input_cb_inflight);
|
||||
(void)__atomic_store_n(&s_back_pressed, 0, __ATOMIC_RELAXED);
|
||||
|
||||
if(input_events) {
|
||||
furi_record_close(RECORD_INPUT_EVENTS);
|
||||
input_events = NULL;
|
||||
}
|
||||
st->input_events = NULL;
|
||||
|
||||
if(gui) {
|
||||
gui_remove_framebuffer_callback(gui, framebuffer_commit_callback, st);
|
||||
}
|
||||
|
||||
wait_inflight_zero(&s_fb_cb_inflight);
|
||||
|
||||
if(gui) {
|
||||
if(canvas) {
|
||||
gui_direct_draw_release(gui);
|
||||
canvas = NULL;
|
||||
}
|
||||
furi_record_close(RECORD_GUI);
|
||||
gui = NULL;
|
||||
}
|
||||
st->gui = NULL;
|
||||
st->canvas = NULL;
|
||||
|
||||
if(st->fb_mutex) {
|
||||
furi_mutex_free(st->fb_mutex);
|
||||
st->fb_mutex = NULL;
|
||||
}
|
||||
|
||||
Platform::SetAudioEnabled(false);
|
||||
|
||||
free(st);
|
||||
g_state = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
extract_doom_assets.py
|
||||
|
||||
Reads the user's own Doom shareware IWAD (Doom1.WAAD is freely distributable
|
||||
as a whole) and converts a selection of its sprites into 1-bit assets in the
|
||||
FlipperCatacombs engine formats. Nothing from the WAD is stored in this
|
||||
repository: the header is generated locally at build time from the WAD the
|
||||
user already has.
|
||||
|
||||
Usage:
|
||||
python3 tools/extract_doom_assets.py <path/to/Doom1.WAD> [output_header]
|
||||
|
||||
Output (default): game/Generated/DoomSprites.inc.h
|
||||
|
||||
Engine formats
|
||||
--------------
|
||||
1) Scaled sprite (16x16), uint16_t array, per frame:
|
||||
16 columns x 2 words: [transparency mask, colour], bit v = row v (bit0=top)
|
||||
2) Page sprite (Platform::DrawSprite): uint8_t array:
|
||||
w, h, then per page (8 rows), per column: [colour byte, mask byte]
|
||||
(bit0 = top row of the page)
|
||||
3) Solid bitmap (Platform::DrawSolidBitmap): uint8_t array:
|
||||
w, h, then per page, per column: colour byte where bit=1 means BLACK
|
||||
(DrawSolidBitmap writes fill = ~src)
|
||||
4) HUD icon: 8 raw page bytes (bit=1 -> white pixel)
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
# ---------------------------------------------------------------- WAD parsing
|
||||
|
||||
|
||||
class Wad:
|
||||
def __init__(self, path):
|
||||
self.data = open(path, "rb").read()
|
||||
ident, numlumps, diroff = struct.unpack_from("<4sII", self.data, 0)
|
||||
if ident not in (b"IWAD", b"PWAD"):
|
||||
raise ValueError("Not a WAD file")
|
||||
self.lumps = {}
|
||||
for i in range(numlumps):
|
||||
off, size, name = struct.unpack_from("<II8s", self.data, diroff + 16 * i)
|
||||
name = name.rstrip(b"\0").decode("ascii", "replace")
|
||||
# keep first occurrence (IWAD order)
|
||||
if name not in self.lumps:
|
||||
self.lumps[name] = (off, size)
|
||||
|
||||
def lump(self, name):
|
||||
off, size = self.lumps[name]
|
||||
return self.data[off : off + size]
|
||||
|
||||
def has(self, name):
|
||||
return name in self.lumps
|
||||
|
||||
|
||||
def load_palette(wad):
|
||||
pal = wad.lump("PLAYPAL")[:768]
|
||||
grays = []
|
||||
for i in range(256):
|
||||
r, g, b = pal[i * 3], pal[i * 3 + 1], pal[i * 3 + 2]
|
||||
grays.append(0.299 * r + 0.587 * g + 0.114 * b)
|
||||
return grays
|
||||
|
||||
|
||||
def decode_picture(wad, name, grays):
|
||||
"""Decode Doom picture format -> (w, h, pixels) where pixels is a list of
|
||||
rows; each entry is None (transparent) or gray 0..255."""
|
||||
raw = wad.lump(name)
|
||||
w, h, _lo, _to = struct.unpack_from("<hhhh", raw, 0)
|
||||
colofs = struct.unpack_from("<%di" % w, raw, 8)
|
||||
pix = [[None] * w for _ in range(h)]
|
||||
for x in range(w):
|
||||
p = colofs[x]
|
||||
while raw[p] != 0xFF:
|
||||
topdelta = raw[p]
|
||||
length = raw[p + 1]
|
||||
p += 3 # topdelta, length, pad
|
||||
for i in range(length):
|
||||
y = topdelta + i
|
||||
if 0 <= y < h:
|
||||
pix[y][x] = grays[raw[p]]
|
||||
p += 1
|
||||
p += 1 # trailing pad
|
||||
return w, h, pix
|
||||
|
||||
|
||||
# ------------------------------------------------------------- image helpers
|
||||
|
||||
BAYER4 = [
|
||||
[0, 8, 2, 10],
|
||||
[12, 4, 14, 6],
|
||||
[3, 11, 1, 9],
|
||||
[15, 7, 13, 5],
|
||||
]
|
||||
|
||||
|
||||
def bbox(pix):
|
||||
xs, ys = [], []
|
||||
for y, row in enumerate(pix):
|
||||
for x, v in enumerate(row):
|
||||
if v is not None:
|
||||
xs.append(x)
|
||||
ys.append(y)
|
||||
return min(xs), min(ys), max(xs) + 1, max(ys) + 1
|
||||
|
||||
|
||||
def crop(pix, x0, y0, x1, y1):
|
||||
return [row[x0:x1] for row in pix[y0:y1]]
|
||||
|
||||
|
||||
def box_resize(pix, nw, nh):
|
||||
"""Box-filter resize of (gray|None) grid; alpha = coverage."""
|
||||
h = len(pix)
|
||||
w = len(pix[0])
|
||||
out_gray = [[0.0] * nw for _ in range(nh)]
|
||||
out_alpha = [[0.0] * nw for _ in range(nh)]
|
||||
for ny in range(nh):
|
||||
sy0 = ny * h / nh
|
||||
sy1 = (ny + 1) * h / nh
|
||||
for nx in range(nw):
|
||||
sx0 = nx * w / nw
|
||||
sx1 = (nx + 1) * w / nw
|
||||
acc_g = acc_a = acc_w = 0.0
|
||||
y = int(sy0)
|
||||
while y < sy1 and y < h:
|
||||
wy = min(sy1, y + 1) - max(sy0, y)
|
||||
x = int(sx0)
|
||||
while x < sx1 and x < w:
|
||||
wx = min(sx1, x + 1) - max(sx0, x)
|
||||
weight = wx * wy
|
||||
acc_w += weight
|
||||
v = pix[y][x]
|
||||
if v is not None:
|
||||
acc_a += weight
|
||||
acc_g += weight * v
|
||||
x += 1
|
||||
y += 1
|
||||
if acc_w > 0 and acc_a > 0:
|
||||
out_gray[ny][nx] = acc_g / acc_a
|
||||
out_alpha[ny][nx] = acc_a / acc_w
|
||||
return out_gray, out_alpha
|
||||
|
||||
|
||||
def normalize(gray, alpha, thresh=0.5):
|
||||
"""Per-sprite contrast stretch over opaque pixels."""
|
||||
vals = [
|
||||
gray[y][x]
|
||||
for y in range(len(gray))
|
||||
for x in range(len(gray[0]))
|
||||
if alpha[y][x] >= thresh
|
||||
]
|
||||
if not vals:
|
||||
return gray
|
||||
lo, hi = min(vals), max(vals)
|
||||
if hi - lo < 1e-6:
|
||||
hi = lo + 1.0
|
||||
return [
|
||||
[(v - lo) * 255.0 / (hi - lo) for v in row]
|
||||
for row in gray
|
||||
]
|
||||
|
||||
|
||||
def to_1bit(gray, alpha, bias=0.0):
|
||||
"""3-tone quantization (black / 50% checker / white) for clean tiny
|
||||
sprites. Returns (colour, mask) row-major bools."""
|
||||
h, w = len(gray), len(gray[0])
|
||||
colour = [[False] * w for _ in range(h)]
|
||||
mask = [[False] * w for _ in range(h)]
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if alpha[y][x] >= 0.5:
|
||||
mask[y][x] = True
|
||||
v = gray[y][x] + bias
|
||||
if v < 80:
|
||||
colour[y][x] = False
|
||||
elif v < 175:
|
||||
colour[y][x] = ((x + y) & 1) == 0
|
||||
else:
|
||||
colour[y][x] = True
|
||||
return colour, mask
|
||||
|
||||
|
||||
def fit_grid(pix, size, valign, hpad=0):
|
||||
"""Crop to bbox, keep aspect, fit into size x size grid.
|
||||
valign: 'bottom' or 'center'."""
|
||||
x0, y0, x1, y1 = bbox(pix)
|
||||
pix = crop(pix, x0, y0, x1, y1)
|
||||
w = x1 - x0
|
||||
h = y1 - y0
|
||||
avail = size - hpad * 2
|
||||
if w >= h:
|
||||
nw = avail
|
||||
nh = max(1, round(h * avail / w))
|
||||
else:
|
||||
nh = avail
|
||||
nw = max(1, round(w * avail / h))
|
||||
gray, alpha = box_resize(pix, nw, nh)
|
||||
# paste into size x size
|
||||
g = [[0.0] * size for _ in range(size)]
|
||||
a = [[0.0] * size for _ in range(size)]
|
||||
ox = (size - nw) // 2
|
||||
oy = (size - nh) if valign == "bottom" else (size - nh) // 2
|
||||
for y in range(nh):
|
||||
for x in range(nw):
|
||||
g[oy + y][ox + x] = gray[y][x]
|
||||
a[oy + y][ox + x] = alpha[y][x]
|
||||
return g, a
|
||||
|
||||
|
||||
# ------------------------------------------------------------ format emitters
|
||||
|
||||
|
||||
def emit_scaled16(frames):
|
||||
"""frames: list of (colour, mask) 16x16 row-major -> list of uint16 words."""
|
||||
words = []
|
||||
for colour, mask in frames:
|
||||
for x in range(16):
|
||||
t = c = 0
|
||||
for y in range(16):
|
||||
if mask[y][x]:
|
||||
t |= 1 << y
|
||||
if colour[y][x]:
|
||||
c |= 1 << y
|
||||
words.append(t)
|
||||
words.append(c)
|
||||
return words
|
||||
|
||||
|
||||
def emit_page_sprite(colour, mask):
|
||||
"""-> list of bytes: w, h, then per page per column [colour, mask]."""
|
||||
h, w = len(colour), len(colour[0])
|
||||
pages = (h + 7) // 8
|
||||
out = [w, h]
|
||||
for page in range(pages):
|
||||
for x in range(w):
|
||||
cb = mb = 0
|
||||
for bit in range(8):
|
||||
y = page * 8 + bit
|
||||
if y < h and mask[y][x]:
|
||||
mb |= 1 << bit
|
||||
if colour[y][x]:
|
||||
cb |= 1 << bit
|
||||
out.append(cb)
|
||||
out.append(mb)
|
||||
return out
|
||||
|
||||
|
||||
def emit_solid_bitmap(colour):
|
||||
"""DrawSolidBitmap: bit=1 -> black. colour True = white pixel."""
|
||||
h, w = len(colour), len(colour[0])
|
||||
pages = (h + 7) // 8
|
||||
out = [w, h]
|
||||
for page in range(pages):
|
||||
for x in range(w):
|
||||
b = 0
|
||||
for bit in range(8):
|
||||
y = page * 8 + bit
|
||||
if y < h and not colour[y][x]:
|
||||
b |= 1 << bit
|
||||
out.append(b)
|
||||
return out
|
||||
|
||||
|
||||
def fmt_words(words, per_line=16):
|
||||
lines = []
|
||||
for i in range(0, len(words), per_line):
|
||||
lines.append(",".join("0x%x" % v for v in words[i : i + per_line]))
|
||||
return ",\n\t".join(lines)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ pipeline
|
||||
|
||||
|
||||
def sprite16(wad, grays, names, valign, bias=0.0):
|
||||
frames = []
|
||||
for n in names:
|
||||
w, h, pix = decode_picture(wad, n, grays)
|
||||
g, a = fit_grid(pix, 16, valign)
|
||||
g = normalize(g, a)
|
||||
frames.append(to_1bit(g, a, bias))
|
||||
return emit_scaled16(frames)
|
||||
|
||||
|
||||
def first_present(wad, *names):
|
||||
for n in names:
|
||||
if wad.has(n):
|
||||
return n
|
||||
raise KeyError("none of %s in WAD" % (names,))
|
||||
|
||||
|
||||
def build_weapon(wad, grays, target_w=46):
|
||||
"""Idle shotgun + firing frame (shotgun with muzzle flash composited)."""
|
||||
w, h, gun = decode_picture(wad, "SHTGA0", grays)
|
||||
x0, y0, x1, y1 = bbox(gun)
|
||||
gun = crop(gun, x0, y0, x1, y1)
|
||||
gw, gh = x1 - x0, y1 - y0
|
||||
nw = target_w
|
||||
nh = max(1, round(gh * nw / gw))
|
||||
g, a = box_resize(gun, nw, nh)
|
||||
g = normalize(g, a)
|
||||
# limit height so it doesn't cover too much of the 64px screen: keep the
|
||||
# top rows (barrel); the grip sticks out of the screen bottom like in Doom
|
||||
max_h = 28
|
||||
if nh > max_h:
|
||||
g = g[:max_h]
|
||||
a = a[:max_h]
|
||||
nh = max_h
|
||||
idle = to_1bit(g, a)
|
||||
|
||||
# firing frame: muzzle flash above the barrel
|
||||
fname = first_present(wad, "SHTFB0", "SHTFA0")
|
||||
fw, fh, fl = decode_picture(wad, fname, grays)
|
||||
fx0, fy0, fx1, fy1 = bbox(fl)
|
||||
fl = crop(fl, fx0, fy0, fx1, fy1)
|
||||
fsw = max(1, round((fx1 - fx0) * nw / gw))
|
||||
fsh = max(1, round((fy1 - fy0) * nw / gw))
|
||||
fg, fa = box_resize(fl, fsw, fsh)
|
||||
fg = normalize(fg, fa)
|
||||
# keep total height reasonable: crop the top of the flash if needed
|
||||
max_total = 38
|
||||
if nh + fsh > max_total:
|
||||
cut = nh + fsh - max_total
|
||||
fg = fg[cut:]
|
||||
fa = fa[cut:]
|
||||
fsh -= cut
|
||||
|
||||
# find barrel top-center of scaled gun: centroid of top opaque row
|
||||
top_row = 0
|
||||
for y in range(nh):
|
||||
if any(a[y][x] >= 0.5 for x in range(nw)):
|
||||
top_row = y
|
||||
break
|
||||
cols = [x for x in range(nw) if a[top_row][x] >= 0.5]
|
||||
cx = sum(cols) // len(cols) if cols else nw // 2
|
||||
|
||||
fire_h = nh + fsh
|
||||
FG = [[0.0] * nw for _ in range(fire_h)]
|
||||
FA = [[0.0] * nw for _ in range(fire_h)]
|
||||
for y in range(nh):
|
||||
for x in range(nw):
|
||||
FG[fsh + y][x] = g[y][x]
|
||||
FA[fsh + y][x] = a[y][x]
|
||||
ox = cx - fsw // 2
|
||||
for y in range(fsh):
|
||||
for x in range(fsw):
|
||||
dx = ox + x
|
||||
if 0 <= dx < nw and fa[y][x] >= 0.5:
|
||||
FG[y][dx] = fg[y][x]
|
||||
FA[y][dx] = fa[y][x]
|
||||
fire = to_1bit(FG, FA, bias=40.0) # flash reads brighter
|
||||
|
||||
return emit_page_sprite(*idle), emit_page_sprite(*fire)
|
||||
|
||||
|
||||
TITLE_LETTERS = {
|
||||
"D": [
|
||||
"######.",
|
||||
"##..##.",
|
||||
"##..###",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##..###",
|
||||
"##..##.",
|
||||
"######.",
|
||||
],
|
||||
"O": [
|
||||
".#####.",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
".#####.",
|
||||
],
|
||||
"M": [
|
||||
"##...##",
|
||||
"###.###",
|
||||
"#######",
|
||||
"##.#.##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_title(wad, grays):
|
||||
"""Original blocky 'DOOM' pixel title with dithered gradient, 128x64."""
|
||||
W, H = 128, 64
|
||||
colour = [[False] * W for _ in range(H)]
|
||||
scale = 4 # each letter 7x8 -> 28x32
|
||||
lw, lh = 7 * scale, 8 * scale
|
||||
gap = 4
|
||||
total = 4 * lw + 3 * gap
|
||||
x0 = (W - total) // 2
|
||||
y0 = (H - lh) // 2
|
||||
for i, ch in enumerate("DOOM"):
|
||||
gl = TITLE_LETTERS[ch]
|
||||
ox = x0 + i * (lw + gap)
|
||||
for gy in range(8):
|
||||
for gx in range(7):
|
||||
if gl[gy][gx] != "#":
|
||||
continue
|
||||
for sy in range(scale):
|
||||
for sx in range(scale):
|
||||
x = ox + gx * scale + sx
|
||||
y = y0 + gy * scale + sy
|
||||
# metallic gradient: solid on top, dithered below
|
||||
if y - y0 < lh * 5 // 8:
|
||||
colour[y][x] = True
|
||||
else:
|
||||
colour[y][x] = ((x + y) & 1) == 0
|
||||
return emit_solid_bitmap(colour)
|
||||
|
||||
|
||||
def icon_from_pixmap(rows):
|
||||
"""8x8 pixmap ('#'=white) -> 8 page bytes, bit0 = top row."""
|
||||
out = []
|
||||
for x in range(8):
|
||||
b = 0
|
||||
for y in range(8):
|
||||
if rows[y][x] == "#":
|
||||
b |= 1 << y
|
||||
out.append(b)
|
||||
return out
|
||||
|
||||
|
||||
HEALTH_ICON = [
|
||||
"..###...",
|
||||
"..#.#...",
|
||||
"###.###.",
|
||||
"#.....#.",
|
||||
"###.###.",
|
||||
"..#.#...",
|
||||
"..###...",
|
||||
"........",
|
||||
]
|
||||
|
||||
AMMO_ICON = [
|
||||
".#..#...",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"........",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
wad_path = sys.argv[1] if len(sys.argv) > 1 else "../doomgeneric/Doom1.WAD"
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
out_path = (
|
||||
sys.argv[2]
|
||||
if len(sys.argv) > 2
|
||||
else os.path.join(root, "game", "Generated", "DoomSprites.inc.h")
|
||||
)
|
||||
|
||||
wad = Wad(wad_path)
|
||||
grays = load_palette(wad)
|
||||
|
||||
scaled = [] # (symbol, numFrames, words)
|
||||
|
||||
def add16(symbol, names, valign, bias=0.0):
|
||||
scaled.append((symbol, len(names), sprite16(wad, grays, names, valign, bias)))
|
||||
|
||||
# enemies (2 walk frames each)
|
||||
add16("skeletonSpriteData", ["SARGA1", "SARGB1"], "bottom") # pinky demon
|
||||
add16("mageSpriteData", ["TROOA1", "TROOB1"], "bottom") # imp
|
||||
add16("batSpriteData", ["SPOSA1", "SPOSB1"], "bottom") # shotgun sergeant
|
||||
add16("spiderSpriteData", ["POSSA1", "POSSB1"], "bottom") # zombieman
|
||||
|
||||
# projectiles
|
||||
ball = first_present(wad, "BAL1A0")
|
||||
ball2 = first_present(wad, "BAL1B0", "BAL1A0")
|
||||
add16("projectileSpriteData", [ball], "center", bias=60.0)
|
||||
add16("enemyProjectileSpriteData", [ball2], "center", bias=60.0)
|
||||
|
||||
# decorations / pickups
|
||||
torch1 = first_present(wad, "TREDA0", "CANDA0")
|
||||
torch2 = first_present(wad, "TREDC0", "TREDB0", "CANDA0")
|
||||
add16("torchSpriteData1", [torch1], "center", bias=40.0)
|
||||
add16("torchSpriteData2", [torch2], "center", bias=40.0)
|
||||
add16("urnSpriteData", ["BAR1A0"], "bottom") # barrel
|
||||
add16("potionSpriteData", ["STIMA0"], "bottom", bias=30.0) # stimpack
|
||||
add16("chestSpriteData", ["BPAKA0"], "bottom", bias=30.0) # backpack
|
||||
add16("chestOpenSpriteData", ["CLIPA0"], "bottom", bias=30.0)
|
||||
add16("scrollSpriteData", ["BON2A0"], "bottom", bias=30.0) # armor helmet
|
||||
add16("coinsSpriteData", ["BON1A0"], "bottom", bias=30.0) # potion bottle
|
||||
add16("crownSpriteData", ["ARM1A0"], "bottom", bias=30.0) # green armor
|
||||
add16("signSpriteData", ["POL5A0"], "bottom", bias=20.0) # skull pile
|
||||
|
||||
weapon_idle, weapon_fire = build_weapon(wad, grays)
|
||||
title = build_title(wad, grays)
|
||||
health = icon_from_pixmap(HEALTH_ICON)
|
||||
ammo = icon_from_pixmap(AMMO_ICON)
|
||||
|
||||
with open(out_path, "w") as f:
|
||||
f.write("// Auto-generated by tools/extract_doom_assets.py\n")
|
||||
f.write("// Derived at build time from the user's local Doom shareware WAD.\n")
|
||||
f.write("// Do not commit WAD-derived data to public repositories.\n\n")
|
||||
for symbol, nframes, words in scaled:
|
||||
f.write("constexpr uint8_t %s_numFrames = %d;\n" % (symbol, nframes))
|
||||
f.write("extern const uint16_t %s[] PROGMEM =\n{\n\t%s\n};\n" % (symbol, fmt_words(words)))
|
||||
f.write("extern const uint8_t handSpriteData1[] PROGMEM =\n{\n\t%s\n};\n" % fmt_words(weapon_idle, 24))
|
||||
f.write("extern const uint8_t handSpriteData2[] PROGMEM =\n{\n\t%s\n};\n" % fmt_words(weapon_fire, 24))
|
||||
f.write("extern const uint8_t titleBitmapData[] PROGMEM =\n{\n\t%s\n};\n" % fmt_words(title, 24))
|
||||
f.write("extern const uint8_t heartSpriteData[] PROGMEM =\n{\n%s\n};\n" % fmt_words(health))
|
||||
f.write("extern const uint8_t manaSpriteData[] PROGMEM =\n{\n%s\n};\n" % fmt_words(ammo))
|
||||
|
||||
total = sum(len(w) * 2 for _, _, w in scaled) + len(weapon_idle) + len(weapon_fire) + len(title) + 16
|
||||
print("Wrote %s (%d bytes of asset data)" % (out_path, total))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 671 B |
|
After Width: | Height: | Size: 1016 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 287 B |
@@ -0,0 +1,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, ®istry) || !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, ®istry) || !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(
|
||||
©[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 we’re already on the target device, don’t 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
|
||||
|
After Width: | Height: | Size: 448 B |
|
After Width: | Height: | Size: 385 B |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 994 B |
|
After Width: | Height: | Size: 3.5 KiB |