commeownder/simulator/main.c

258 lines
7.9 KiB
C

#include <glib.h>
#include <glib-unix.h>
#include <gio/gio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include "adapter.h"
#include "advertisement.h"
#include "device.h"
#include "logger.h"
#define TAG "commeownder-sim"
#define MANUFACTURER_ID 0xFFFF
#define MAGIC_0 0xC0
#define MAGIC_1 0xDE
#define PLAYER_NAME_LEN 8
#pragma pack(push, 1)
typedef struct {
uint8_t magic[2];
uint8_t game_id[2];
char name[PLAYER_NAME_LEN];
int16_t life;
uint8_t poison;
uint8_t eliminated;
uint8_t reset_cmd;
} ble_payload_t;
#pragma pack(pop)
static GMainLoop *loop = NULL;
static Adapter *default_adapter = NULL;
static Advertisement *adv = NULL;
static char g_name[PLAYER_NAME_LEN + 1] = {0};
static int16_t g_life = 40;
static int16_t g_start_life = 40;
static uint8_t g_poison = 0;
static uint8_t g_game_id[2] = {0x42, 0x42};
static uint8_t g_eliminated = 0;
static uint8_t g_reset_cmd = 0;
// Monotonic time of last processed reset (microseconds); 0 = never
static gint64 g_last_reset_us = 0;
#define RESET_COOLDOWN_US (10LL * 1000000LL) // 10 seconds
static void start_advertising(void) {
ble_payload_t payload;
memset(&payload, 0, sizeof(payload));
payload.magic[0] = MAGIC_0;
payload.magic[1] = MAGIC_1;
payload.game_id[0] = g_game_id[0];
payload.game_id[1] = g_game_id[1];
strncpy(payload.name, g_name, PLAYER_NAME_LEN);
payload.life = g_life;
payload.poison = g_poison;
payload.eliminated = g_eliminated;
payload.reset_cmd = g_reset_cmd;
GByteArray *data = g_byte_array_new();
g_byte_array_append(data, (const guint8 *)&payload, sizeof(payload));
adv = binc_advertisement_create();
binc_advertisement_set_manufacturer_data(adv, MANUFACTURER_ID, data);
binc_advertisement_set_interval(adv, 500, 500);
binc_advertisement_set_general_discoverable(adv, FALSE);
g_byte_array_free(data, TRUE);
binc_adapter_start_advertising(default_adapter, adv);
fprintf(stderr, "advertising: name='%s' life=%d poison=%u game_id=%02X%02X reset=%u\n",
g_name, (int)g_life, (unsigned)g_poison,
g_game_id[0], g_game_id[1], (unsigned)g_reset_cmd);
}
static void restart_advertising(void) {
if (adv) {
binc_adapter_stop_advertising(default_adapter, adv);
binc_advertisement_free(adv);
adv = NULL;
}
start_advertising();
}
static void on_scan_result(Adapter *adapter, Device *device) {
(void)adapter;
GHashTable *mfr_data = binc_device_get_manufacturer_data(device);
if (!mfr_data) return;
int key = MANUFACTURER_ID;
GByteArray *data = g_hash_table_lookup(mfr_data, &key);
if (!data || data->len < (guint)sizeof(ble_payload_t)) return;
const ble_payload_t *p = (const ble_payload_t *)data->data;
if (p->magic[0] != MAGIC_0 || p->magic[1] != MAGIC_1) return;
if (p->game_id[0] != g_game_id[0] || p->game_id[1] != g_game_id[1]) return;
char name[PLAYER_NAME_LEN + 1];
memcpy(name, p->name, PLAYER_NAME_LEN);
name[PLAYER_NAME_LEN] = '\0';
printf("PEER addr=%s name=%-8s life=%-5d poison=%u%s%s\n",
binc_device_get_address(device), name,
(int)p->life, (unsigned)p->poison,
p->eliminated ? " ELIMINATED" : "",
p->reset_cmd ? " RESET_CMD" : "");
fflush(stdout);
// Handle reset_cmd with cooldown to avoid acting on repeated broadcasts
if (p->reset_cmd) {
gint64 now = g_get_monotonic_time();
if (now - g_last_reset_us >= RESET_COOLDOWN_US) {
g_last_reset_us = now;
g_life = g_start_life;
g_poison = 0;
g_eliminated = 0;
printf("RESET_ALL life=%d\n", (int)g_life);
fflush(stdout);
restart_advertising();
}
}
}
static void on_powered_state_changed(Adapter *adapter, gboolean state) {
if (!state) return;
start_advertising();
binc_adapter_set_discovery_filter(adapter, -100, NULL, NULL);
binc_adapter_set_discovery_cb(adapter, on_scan_result);
binc_adapter_start_discovery(adapter);
fprintf(stderr, "scanning for peers...\n");
}
static void cleanup(void) {
if (adv) {
binc_adapter_stop_advertising(default_adapter, adv);
binc_advertisement_free(adv);
adv = NULL;
}
if (default_adapter) {
binc_adapter_stop_discovery(default_adapter);
binc_adapter_free(default_adapter);
default_adapter = NULL;
}
if (loop) g_main_loop_quit(loop);
}
static gboolean cleanup_handler(gpointer user_data) {
(void)user_data;
cleanup();
return G_SOURCE_REMOVE;
}
static void print_usage(const char *prog) {
fprintf(stderr,
"Usage: %s --name <name> [--life <n>] [--poison <n>] [--game-id <hex4>] [--eliminated] [--reset-cmd]\n"
" --name Player name (max 8 chars, required)\n"
" --life Life total (default: 40)\n"
" --poison Poison counters 0-9 (default: 0)\n"
" --game-id 4-hex-digit game ID (default: 4242)\n"
" --eliminated Mark player as eliminated\n"
" --reset-cmd Advertise reset_cmd=1 (tells peers to reset counters)\n",
prog);
}
int main(int argc, char *argv[]) {
log_set_level(LOG_WARN);
static const struct option long_opts[] = {
{"name", required_argument, NULL, 'n'},
{"life", required_argument, NULL, 'l'},
{"poison", required_argument, NULL, 'p'},
{"game-id", required_argument, NULL, 'g'},
{"eliminated", no_argument, NULL, 'e'},
{"reset-cmd", no_argument, NULL, 'r'},
{NULL, 0, NULL, 0}
};
int name_set = 0;
int opt;
while ((opt = getopt_long(argc, argv, "", long_opts, NULL)) != -1) {
switch (opt) {
case 'n':
strncpy(g_name, optarg, PLAYER_NAME_LEN);
g_name[PLAYER_NAME_LEN] = '\0';
name_set = 1;
break;
case 'l':
g_life = (int16_t)atoi(optarg);
break;
case 'p':
g_poison = (uint8_t)atoi(optarg);
break;
case 'g': {
unsigned int id;
if (sscanf(optarg, "%x", &id) != 1) {
fprintf(stderr, "invalid game-id '%s' (expected 4 hex digits)\n", optarg);
return 1;
}
g_game_id[0] = (uint8_t)((id >> 8) & 0xFF);
g_game_id[1] = (uint8_t)(id & 0xFF);
break;
}
case 'e':
g_eliminated = 1;
break;
case 'r':
g_reset_cmd = 1;
break;
default:
print_usage(argv[0]);
return 1;
}
}
if (!name_set) {
print_usage(argv[0]);
return 1;
}
g_start_life = g_life;
g_unix_signal_add(SIGINT, cleanup_handler, NULL);
g_unix_signal_add(SIGTERM, cleanup_handler, NULL);
GDBusConnection *dbusConnection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL);
if (!dbusConnection) {
fprintf(stderr, "failed to connect to D-Bus\n");
return 1;
}
loop = g_main_loop_new(NULL, FALSE);
default_adapter = binc_adapter_get_default(dbusConnection);
if (!default_adapter) {
fprintf(stderr, "no Bluetooth adapter found\n");
return 1;
}
fprintf(stderr, "adapter: %s (%s)\n",
binc_adapter_get_name(default_adapter),
binc_adapter_get_address(default_adapter));
binc_adapter_set_powered_state_cb(default_adapter, on_powered_state_changed);
if (binc_adapter_get_powered_state(default_adapter)) {
on_powered_state_changed(default_adapter, TRUE);
} else {
binc_adapter_power_on(default_adapter);
}
g_main_loop_run(loop);
g_dbus_connection_close_sync(dbusConnection, NULL, NULL);
g_object_unref(dbusConnection);
g_main_loop_unref(loop);
return 0;
}