#include #include #include #include #include #include #include #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 #define MAX_OPPONENTS 4 #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 player_id; uint8_t reset_cmd; uint8_t cmdr_dmg[MAX_OPPONENTS]; } 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_player_id = 0; static uint8_t g_reset_cmd = 0; static uint8_t g_cmdr_dmg[MAX_OPPONENTS] = {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.player_id = g_player_id; payload.reset_cmd = g_reset_cmd; memcpy(payload.cmdr_dmg, g_cmdr_dmg, MAX_OPPONENTS); 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 pid=%u reset=%u cmdr=[%u,%u,%u,%u]\n", g_name, (int)g_life, (unsigned)g_poison, g_game_id[0], g_game_id[1], (unsigned)g_player_id, (unsigned)g_reset_cmd, g_cmdr_dmg[0], g_cmdr_dmg[1], g_cmdr_dmg[2], g_cmdr_dmg[3]); } 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 pid=%u cmdr=[%u,%u,%u,%u]%s%s\n", binc_device_get_address(device), name, (int)p->life, (unsigned)p->poison, (unsigned)p->player_id, (unsigned)p->cmdr_dmg[0], (unsigned)p->cmdr_dmg[1], (unsigned)p->cmdr_dmg[2], (unsigned)p->cmdr_dmg[3], 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 [--life ] [--poison ] [--game-id ] [--player-id ] [--eliminated] [--reset-cmd] [--cmdr-dmg :]...\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" " --player-id Player slot 0-4 (default: 0)\n" " --eliminated Mark player as eliminated\n" " --reset-cmd Advertise reset_cmd=1 (tells peers to reset counters)\n" " --cmdr-dmg Commander damage from opponent slot idx (0-3), e.g. --cmdr-dmg 0:7 (repeatable)\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'}, {"player-id", required_argument, NULL, 'i'}, {"eliminated", no_argument, NULL, 'e'}, {"reset-cmd", no_argument, NULL, 'r'}, {"cmdr-dmg", required_argument, NULL, 'd'}, {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 'i': g_player_id = (uint8_t)atoi(optarg); break; case 'e': g_eliminated = 1; break; case 'r': g_reset_cmd = 1; break; case 'd': { int idx, val; if (sscanf(optarg, "%d:%d", &idx, &val) != 2 || idx < 0 || idx >= MAX_OPPONENTS || val < 0 || val > 255) { fprintf(stderr, "invalid cmdr-dmg '%s' (expected :, idx 0-%d, val 0-255)\n", optarg, MAX_OPPONENTS - 1); return 1; } g_cmdr_dmg[idx] = (uint8_t)val; 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; }