258 lines
7.9 KiB
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;
|
|
}
|