Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 87 additions & 19 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,28 +1,96 @@
GAMA_WS_PORT=1000
GAMA_IP_ADDRESS=localhost
# =============================================================================
# SIMPLE WebPlatform — configuration reference
#
# Copy this file to `.env` and adjust the values to your setup.
# All boolean options accept `true`, `1` or `yes` (case-insensitive);
# anything else is treated as `false`.
#
# Remove the leading "#" to apply the value you want to each option
# =============================================================================

HEADSET_WS_PORT=8080
# =============================================================================
# GAMA simulation server
# =============================================================================

MONITOR_WS_PORT=8001
# WebSocket port of the GAMA headless server the platform connects to
# Default: 1000
#GAMA_WS_PORT=1000

WEB_APPLICATION_HOST=0.0.0.0
# IP address (or hostname) of the machine running GAMA
# Default: localhost
#GAMA_IP_ADDRESS=localhost

# Run the platform WITHOUT a GAMA simulation (headset management/streaming only).
# When enabled, the GAMA connection and learning packages are not used.
# Legacy alias `GAMALESS` is also supported.
# Default: false
#GAMALESS=false

# Pro-actively remove a player entirely from GAMA / the middleware as soon as
# its device disconnects (instead of keeping it for a later reconnection)
# Default: false
#AGGRESSIVE_DISCONNECT=false

# =============================================================================
# Learning packages (GAMA models)
# =============================================================================

# Folder scanned for learning packages (GAMA models + settings) at startup.
# Relative paths are resolved from the executable/working directory.
# Mandatory while using the platform with GAMA
# Default: ./learning-packages
#LEARNING_PACKAGE_PATH="./learning-packages"

# (Optional) Additional folder scanned for learning packages, on top of
# LEARNING_PACKAGE_PATH (e.g. a personal models folder outside the repo)
# Default: (unset — disabled)
#EXTRA_LEARNING_PACKAGE_PATH="/uncomment/and/set/another/path/to/check"

# =============================================================================
# Headsets (Unity VR clients)
# =============================================================================

# WebSocket port the platform listens on for Unity headset connections
# !! DO NOT MODIFY THIS ONE !!
# Default: 8080
#HEADSET_WS_PORT=8080

# IP addresses of headsets the middleware should manage/stream (scrcpy).
# Separate each IP with a ";" as shown below.
# When unset, the platform auto-detects headsets on the local network instead.
# Default: (unset — auto-detection)
#HEADSETS_IP="192.168.68.101;192.168.68.102;"

# =============================================================================
# Web application (admin UI)
# =============================================================================

# Network interface the web servers bind to (0.0.0.0 = all interfaces)
# Default: 0.0.0.0
#WEB_APPLICATION_HOST=0.0.0.0

# HTTP port serving the web admin UI
# Default: 8000 (setup wizard) / 5173 when unset (Vite dev server default)
WEB_APPLICATION_PORT=8000
# Allow reaching webplatform with any mDNS hostname
# http://<WEB_HOSTNAME>.local:<WEB_APPLICATION_PORT>
# => Default to http://simple.local:5173
#WEB_HOSTNAME=simple

VERBOSE=false
# WebSocket port used by the admin UI for live monitoring data
# (headset status, streaming state, etc.)
# Default: 8001
#MONITOR_WS_PORT=8001

LEARNING_PACKAGE_PATH="./learning-packages"
# Be careful, this will display A LOT of messages
#EXTRA_LEARNING_PACKAGE_PATH="/uncommment/and/set/another/path/to/check"
# WebSocket port streaming the headsets' video (scrcpy) to the admin UI
# Default: 8082
#VIDEO_WS_PORT=8082

# List IP addresses of headsets you want the middleware to scrcpy
# Each IP should be separated with a ";" as shown below
#HEADSETS_IP="192.168.68.178;192.168.68.180;"
# =============================================================================
# Debug / logging
# =============================================================================

# Will pro-actively entirely remove player from GAMA / Middleware if device disconnect
# Default to `false`
#AGGRESSIVE_DISCONNECT=false
# Enable debug-level logging
# Default: false
#VERBOSE=false

# Enable trace-level logging (implies VERBOSE).
# Be careful, this will display **A LOT** of messages.
# Default: false
#EXTRA_VERBOSE=false
4 changes: 3 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ jobs:

- name: Get release metadata
id: meta
run: echo "date=$(date +'%d/%m/%y %R')" >> "$GITHUB_OUTPUT"
run: |
echo "date=$(date +'%d/%m/%y %R')" >> "$GITHUB_OUTPUT"
chmod +x release-files/*

- name: Publish to Github
uses: softprops/action-gh-release@v2
Expand Down
11 changes: 9 additions & 2 deletions src/api/android/adb/AdbManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,10 @@ export class AdbManager {
}

async startNewStream(device: Device) {
if (!this.isDeviceReady(device)) return;
if (!this.isDeviceReady(device)) {
logger.debug(`[${device.serial}] Not ready to interact with ADB. Skipping...`)
return;
}

// Ensure having only one streaming per device — compare by serial, not reference
if (this.clientCurrentlyStreaming.some((d) => d.serial === device.serial)) {
Expand Down Expand Up @@ -214,7 +217,10 @@ export class AdbManager {
isDeviceReady(device: Device): boolean {
let isReady = false;

switch (device.state) {

if (device.serial.endsWith("._adb-tls-connect._tcp"))
logger.debug(`[${device.serial}] Not a real device. Skipping...`);
else switch (device.state) {
case "device":
isReady = true;
break;
Expand All @@ -233,6 +239,7 @@ export class AdbManager {
default:
logger.warn(`[${device.serial}] Device is not ready with an unknown state (${device.state}), skipping`);
}

return isReady;
}

Expand Down
19 changes: 19 additions & 0 deletions src/api/core/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@
"UnableToExecuteRequest",
];

// Foreign-experiment probing ==============================================
// Evaluated against an experiment the platform didn't launch (e.g. opened in
// GAMA's GUI) to check whether it is actually usable: `cycle` fails unless a
// live simulation exists, and `paused` captures the current run state.
// Successful reply content looks like "false|3565" (paused|cycle).
export const GAMA_PROBE_EXPR = 'string(paused) + "|" + string(cycle)';
export const GAMA_PROBE_TIMEOUT_MS = 3000;

/**
* Extract the player id from a `do create_player("...")` / `do remove_player("...")`
* expression echoed back by Gama Server in its command payloads.
* @returns the player id, or null if the expression is not a create_player call
*/
export function extractCreatePlayerId(expr: unknown): string | null {
if (typeof expr !== "string") return null;
const match = expr.match(/do create_player\("([^"]+)"\)/);

Check warning on line 96 in src/api/core/Constants.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "RegExp.exec()" method instead.

See more on https://sonarcloud.io/project/issues?id=project-SIMPLE_simple.webplatform&issues=AZ9vOD19PXTnMdrj78Ae&open=AZ9vOD19PXTnMdrj78Ae&pullRequest=165
return match ? match[1] : null;
}

/**
* ANSI colors for console output
*/
Expand Down
5 changes: 4 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,13 @@
? ["true", "1", "yes"].includes(process.env.VERBOSE.toLowerCase())
: false;

// Supports legacy `ENV_GAMALESS` and linter `GAMALESS`
ENV_GAMALESS =
process.env.ENV_GAMALESS !== undefined
? ["true", "1", "yes"].includes(process.env.ENV_GAMALESS.toLowerCase())
: false;
: process.env.GAMALESS !== undefined
? ["true", "1", "yes"].includes(process.env.GAMALESS.toLowerCase())
: false;

Check warning on line 151 in src/api/index.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=project-SIMPLE_simple.webplatform&issues=AZ9vOD6cPXTnMdrj78Af&open=AZ9vOD6cPXTnMdrj78Af&pullRequest=165

/*
SETUP LOGGING SYSTEM ================================
Expand Down
18 changes: 13 additions & 5 deletions src/api/multiplayer/PlayerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,7 @@ class PlayerManager {
this.playerList.get(playerWsId)!.date_connection =
`${new Date().getHours().toString().padStart(2, "0")}:${new Date().getMinutes().toString().padStart(2, "0")}`;

if (
this.controller.gama_connector !== undefined &&
!["NONE", "NOTREADY"].includes(this.controller.gama_connector.jsonGamaState.experiment_state)
) {
if (this.controller.gama_connector?.canTalkToExperiment()) {
logger.debug(`Adding player ${this.playerList.get(playerWsId)?.id} to GAMA simulation...`);
this.controller.addInGamePlayer(playerWsId);
this.togglePlayerInGame(playerWsId, true);
Expand Down Expand Up @@ -417,7 +414,7 @@ class PlayerManager {
logger.debug("Add every player at once");

for (const [playerWsId, player] of this.playerList) {
if (this.controller.gama_connector !== undefined && !player.in_game) {
if (this.controller.gama_connector?.canTalkToExperiment() && !player.in_game) {
this.controller.gama_connector.addInGamePlayer(playerWsId);
this.togglePlayerInGame(playerWsId, true);
}
Expand Down Expand Up @@ -497,6 +494,17 @@ class PlayerManager {
}
}

/**
* Sends an explicit error message to a player's client
* (e.g. GAMA rejected its create_player, see issue #156)
* @param {string} playerId - The in-game id of the player
* @param {string} error - Error identifier sent to the client
*/
notifyPlayerError(playerId: string, error: string) {
const playerIP = this.getIndexByPlayerId(playerId);
if (playerIP !== undefined) this.sendMessageByWs(playerIP, { type: "error", content: error });
}

/**
*
* @param playerWsId
Expand Down
Loading
Loading