*/ private array $connections = []; private ?RuntimeClient $client = null; public function boot(App $app): void { $tcp = new Tcp($app->config, $app->http(), fn (): RuntimeLink => $this->runtimeLink($app)); $app->gameLoop->addPeriodicTimer(0.02, fn () => $tcp->poll()); $app->command('settings', fn (mixed $p): array => $this->settings($app)); $app->command('strings', fn (mixed $p): array => $app->config->file('lang.yaml')); $app->command('direct.open', function (mixed $p) use ($app, $tcp): array { $p = is_array($p) ? $p : []; $id = (string)($p['id'] ?? ''); $host = (string)($p['host'] ?? ''); $port = (int)($p['port'] ?? 0); if ($id === '' || !self::validTarget($host, $port)) { return ['ok' => false, 'error' => 'bad target']; } $c = $tcp->connect($host, $port, 3.0); $this->connections[$id] = $c; $send = fn (string $type, array $more = []) => $app->send('direct', ['id' => $id, 'type' => $type] + $more); $c->on('open', fn () => $send('open')); $c->on('data', fn (string $d) => $send('data', ['data' => base64_encode($d)])); $c->on('error', fn (string $m) => $send('error', ['message' => $m])); $c->on('close', function (?string $why) use ($id, $send): void { unset($this->connections[$id]); $send('close', ['message' => (string)$why]); }); return ['ok' => true]; }); $app->command('direct.write', function (mixed $p): bool { $c = $this->connections[(string)($p['id'] ?? '')] ?? null; $bytes = base64_decode((string)($p['data'] ?? ''), true); if ($c === null || $bytes === false) { return false; } $c->write($bytes); return true; }); $app->command('direct.close', function (mixed $p): bool { $c = $this->connections[(string)($p['id'] ?? '')] ?? null; $c?->close(); return $c !== null; }); $app->command('runtime.state', fn (mixed $p): array => ['state' => $this->runtimeLink($app)->state()]); $app->command('runtime.pair', function (mixed $p) use ($app) { $client = $this->runtimeLink($app); return $client instanceof RuntimeClient ? $client->pair($app->config->id(), $app->config->title()) : false; }); } /** @return array{relay_url:string, net_mode:string, native:bool, version:string, direct_head_start:float} */ public function settings(App $app): array { $relay = (string)$app->config->get('ide_relay_url', self::RELAY_DEFAULT); return [ 'relay_url' => $relay !== '' ? $relay : self::RELAY_DEFAULT, 'net_mode' => $app->config->netMode(), 'native' => !Platform::isWasm(), 'version' => (string)$app->config->get('version', ''), 'direct_head_start' => 1.5, ]; } /** A LAN or IPv6 literal / name and a real port: nothing else may be dialled from here. */ public static function validTarget(string $host, int $port): bool { return $port >= 1 && $port <= 65535 && preg_match('/^[A-Za-z0-9._:\-]{1,253}$/', $host) === 1; } /** In the browser: the client of the JumPHP runtime (looks for it by itself); elsewhere NoRuntime-like behaviour (plain sockets). */ private function runtimeLink(App $app): RuntimeLink { if ($this->client === null && $app->runtime() instanceof BrowserRuntime && Platform::isWasm()) { $this->client = new RuntimeClient($app->runtime()->bridge, $app->config); $client = $this->client; $app->gameLoop->addPeriodicTimer(0.05, fn () => $client->update(0.05)); $client->autoConnect(); } return $this->client ?? new \JumPHP\Net\NoRuntime(); } }