ONI Agent Bridge - initial mod implementation

This commit is contained in:
JianFeeeee
2026-05-30 11:00:36 +08:00
commit 1f25d3d879
95 changed files with 50686 additions and 0 deletions

142
scripts/annotate_apis.py Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
DEPRECATED: Annotate uncertain C# APIs in the Mod source with verification markers.
This script was used during the initial prototyping phase. The mod has since been
rewritten to use confirmed API signatures from the actual ONI DLL. Running this
script on the current source will add irrelevant annotations.
"""
import re
with open('mod/ONIAgentBridge.cs', 'r') as f:
content = f.read()
lines = content.split('\n')
annotations = {}
# Track by line number (1-indexed)
for i, line in enumerate(lines, 1):
s = line.strip()
# Skip comments and using directives
if s.startswith('//') or s.startswith('/*') or s.startswith('using ') or s.startswith('#') or s.startswith('*'):
continue
# === SpeedControlScreen ===
if 'SpeedControlScreen.Instance' in s and ('Pause(' in s or 'Unpause(' in s or 'SetSpeed(' in s):
annotations[i] = '🔶 UNVERIFIED: Pause/Unpause/SetSpeed method signatures on SpeedControlScreen'
# === Circuit Manager ===
if 'circuitManager' in s:
annotations[i] = '🔶 UNVERIFIED: Game.Instance.circuitManager — may be electricalManager'
if 'mgr.GetCircuits()' in s:
annotations[i] = '🔶 UNVERIFIED: circuitManager.GetCircuits() method may not exist'
if 'circuit.WattsUsed' in s or 'circuit.WattsGenerated' in s or 'circuit.MaxWatts' in s:
annotations[i] = '🔶 UNVERIFIED: Circuit property names may differ'
if 'gen.WattageRating' in s or 'gen.CircuitID' in s:
annotations[i] = '🔶 UNVERIFIED: Generator property names may differ'
# === Pipes ===
if 'Game.Instance?.liquidConduitFlow' in s:
annotations[i] = '🔶 UNVERIFIED: Game.Instance.liquidConduitFlow — may be different conduit system'
if 'Game.Instance?.gasConduitFlow' in s:
annotations[i] = '🔶 UNVERIFIED: Game.Instance.gasConduitFlow — may be different conduit system'
if 'flow.GetContents(' in s:
annotations[i] = '🔶 UNVERIFIED: ConduitFlow.GetContents() method and return type'
if 'contents.element' in s or 'contents.mass' in s or 'contents.temperature' in s:
annotations[i] = '🔶 UNVERIFIED: ConduitContents property names'
if 'Components.LiquidConduits' in s or 'Components.GasConduits' in s:
annotations[i] = '🔶 UNVERIFIED: Components.LiquidConduits / GasConduits may not exist'
if 'conduit.GetCell()' in s:
annotations[i] = '🔶 UNVERIFIED: Conduit.GetCell() method'
# === Printing Pod ===
if 'pod.IsReady()' in s or 'pod.CyclesUntilReady()' in s:
annotations[i] = '🔶 UNVERIFIED: PrintingPod methods IsReady/CyclesUntilReady — may differ'
if 'pod.GetCurrentOffers()' in s:
annotations[i] = '🔶 UNVERIFIED: PrintingPod.GetCurrentOffers() return type'
if 'offer.GetName()' in s:
annotations[i] = '🔶 UNVERIFIED: CarePackage/PrintingPodOffer.GetName() method'
if 'pod.SelectOffer(' in s:
annotations[i] = '🔶 UNVERIFIED: PrintingPod.SelectOffer(int) method'
if 'ImmuneSystemMonitor.Instance' in s and ('IsReadyToPrint' in s or 'GetCyclesUntilNextPrint' in s):
annotations[i] = '🔶 UNVERIFIED: ImmuneSystemMonitor methods may not exist'
# === Door ===
if 'door.Lock()' in s or 'door.Unlock()' in s:
annotations[i] = '🔶 UNVERIFIED: Door.Lock()/Unlock() methods may not exist'
# === Operational/Toggle ===
if 'oper.SetFlag(Operational.ActiveFlag,' in s:
annotations[i] = '🔶 UNVERIFIED: Operational.SetFlag signature — may need different approach'
# === Storage ===
if 'storage.DropAll(' in s:
annotations[i] = '🔶 UNVERIFIED: Storage.DropAll() parameters may differ'
# === Camera ===
if 'CameraController.Instance' in s and 'SetPosition' not in s:
annotations[i] = '🔶 UNVERIFIED: CameraController.Instance may not be the correct singleton'
if 'Camera.main?.orthographicSize' in s:
annotations[i] = '🔶 UNVERIFIED: Camera.main.orthographicSize setter may not work in ONI'
if 'cam.transform.position' in s and '= new Vector3' in s:
annotations[i] = '🔶 UNVERIFIED: Setting camera transform.position directly may not work'
# === Screenshot ===
if 'ScreenCapture.CaptureScreenshot' in s:
annotations[i] = '🔶 UNVERIFIED: ScreenCapture may not be available in ONI Unity version'
# === Save/Load ===
if 'SaveLoader.Save(' in s or 'SaveLoader.Load(' in s:
annotations[i] = '🔶 UNVERIFIED: SaveLoader.Save/Load method signatures'
if 'SaveLoader.GetActiveSaveFilePath()' in s:
annotations[i] = '🔶 UNVERIFIED: SaveLoader.GetActiveSaveFilePath() may not exist'
# === Research ===
if 'Research.Instance?.GetActiveResearchTechnologies' in s:
annotations[i] = '🔶 UNVERIFIED: Research.GetActiveResearchTechnologies() method'
if 'Research.Instance?.QueueResearch(' in s:
annotations[i] = '🔶 UNVERIFIED: Research.QueueResearch(Tech) method'
if 'Research.Instance?.CancelResearch(' in s:
annotations[i] = '🔶 UNVERIFIED: Research.CancelResearch(Tech) method'
if 'tech.pointsForCompletion' in s:
annotations[i] = '🔶 UNVERIFIED: Tech.pointsForCompletion property'
# === Overlay ===
if 'simOverlayManager' in s:
annotations[i] = '🔶 UNVERIFIED: Game.Instance.simOverlayManager may not exist'
if 'currentOverlay' in s:
annotations[i] = '🔶 UNVERIFIED: currentOverlay.ToString() may not yield expected overlay names'
# === Buildings ===
if 'Assets.BuildingDefs' in s and 'GetBuildingDef' not in s:
annotations[i] = '🔶 UNVERIFIED: Assets.BuildingDefs may not be a collection'
# === Grid ===
if 'Grid.Germs' in s:
annotations[i] = '🔶 UNVERIFIED: Grid.Germs API — may not exist or have different type'
# === DTO class ===
if 'public int? x2' in s or 'public int? y2' in s:
annotations[i] = '🔶 UNVERIFIED: Nullable parameters may not serialize correctly'
# === Buildings detail ===
if 'health?.GetHealth()' in s:
annotations[i] = '🔶 UNVERIFIED: Health component GetHealth()/GetMaxHealth() methods'
# === Sensor ===
if 'AtmoSensor' in s and '?.threshold' in s:
annotations[i] = '🔶 UNVERIFIED: AtmoSensor/ThermoSensor/HydroSensor threshold property'
# Apply annotations — add comment after the line
annotated = 0
for lineno in sorted(annotations.keys(), reverse=True):
idx = lineno - 1
comment = ' // ' + annotations[lineno]
lines[idx] = lines[idx] + comment
annotated += 1
with open('mod/ONIAgentBridge.cs', 'w') as f:
f.write('\n'.join(lines))
print(f"Annotated {annotated} lines with verification markers")

30
scripts/auto_analyze.sh Normal file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# ONI Agent - Quick analysis report
# Runs health check, then pulls full game state and analyzes it.
set -euo pipefail
SCRIPT_DIR="$(dirname "$0")"
TOOLS_DIR="$SCRIPT_DIR/../tools"
echo "========================================"
echo " ONI Agent - Quick Analysis"
echo "========================================"
# Step 1: Health check
echo "[1/3] Checking Mod connection..."
python3 "$TOOLS_DIR/oni_api.py" health 2>/dev/null || {
echo "[!] Game not connected. Run auto_repair.sh first."
exit 1
}
# Step 2: Full status dump
echo "[2/3] Fetching game state..."
python3 "$TOOLS_DIR/oni_api.py" status
# Step 3: Analysis report
echo ""
echo "[3/3] Running analysis..."
python3 "$TOOLS_DIR/oni_analyzer.py"
echo ""
echo "Done."

28
scripts/auto_repair.sh Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# ONI Agent - Auto Repair Script
# Detects game connection issues and attempts to restart the Mod bridge.
set -euo pipefail
CONFIG_PATH="$(dirname "$0")/../config.json"
HOST=$(python3 -c "import json; print(json.load(open('$CONFIG_PATH'))['modHost'])")
PORT=$(python3 -c "import json; print(json.load(open('$CONFIG_PATH'))['modPort'])")
TIMEOUT=$(python3 -c "import json; print(json.load(open('$CONFIG_PATH'))['timeout'])")
check_health() {
curl -sf --max-time "$TIMEOUT" "http://$HOST:$PORT/health" > /dev/null 2>&1
}
echo "[ONI Agent] Checking Mod connection..."
if check_health; then
echo "[OK] Mod is running on $HOST:$PORT"
exit 0
else
echo "[!] Cannot reach Mod at $HOST:$PORT"
echo " Make sure Oxygen Not Included is running with the ONI Agent Bridge mod enabled."
echo " Steps:"
echo " 1. Launch Oxygen Not Included"
echo " 2. Enable 'ONI Agent Bridge' mod in the Mod menu"
echo " 3. Load a save or start a new game"
echo " 4. Run this script again"
exit 1
fi

61
scripts/build_mod.sh Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Build the ONI Agent Bridge Mod
# Requires: mono-complete (for mcs compiler)
# Requires: ONI game installed to get reference DLLs
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
MOD_DIR="$PROJECT_DIR/mod"
OUTPUT_DIR="$MOD_DIR/bin"
ONI_PATH="${ONI_PATH:-}"
if [ -z "$ONI_PATH" ]; then
for candidate in \
"$HOME/.steam/steam/steamapps/common/OxygenNotIncluded" \
"$HOME/.local/share/Steam/steamapps/common/OxygenNotIncluded" \
"/c/Program Files (x86)/Steam/steamapps/common/OxygenNotIncluded" \
"/mnt/c/Program Files (x86)/Steam/steamapps/common/OxygenNotIncluded"; do
MANAGED="$candidate/OxygenNotIncluded_Data/Managed"
if [ -d "$MANAGED" ]; then
ONI_PATH="$MANAGED"
break
fi
done
fi
if [ -z "$ONI_PATH" ]; then
echo "[!] ONI installation not found."
echo " Set ONI_PATH to the Managed directory:"
echo ' export ONI_PATH="/path/to/OxygenNotIncluded_Data/Managed"'
exit 1
fi
echo "[Build] ONI: $ONI_PATH"
for dll in "$ONI_PATH/Assembly-CSharp.dll" "$ONI_PATH/UnityEngine.dll" \
"$ONI_PATH/UnityEngine.CoreModule.dll" "$ONI_PATH/0Harmony.dll"; do
if [ ! -f "$dll" ]; then echo "[!] Missing: $dll"; exit 1; fi
done
mkdir -p "$OUTPUT_DIR"
REFS=""
for dll in "$ONI_PATH/Assembly-CSharp.dll" "$ONI_PATH/UnityEngine.dll" \
"$ONI_PATH/UnityEngine.CoreModule.dll" "$ONI_PATH/0Harmony.dll"; do
REFS="$REFS -reference:\"$dll\""
done
if [ -f "$ONI_PATH/Assembly-CSharp-firstpass.dll" ]; then
REFS="$REFS -reference:\"$ONI_PATH/Assembly-CSharp-firstpass.dll\""
fi
echo "[Build] Compiling..."
eval mcs -target:library -out:"$OUTPUT_DIR/ONIAgentBridge.dll" $REFS -recurse:"$MOD_DIR/*.cs"
echo "[Build] SUCCESS: $OUTPUT_DIR/ONIAgentBridge.dll"
echo ""
echo "Install (Linux):"
echo " MOD_DIR=~/.config/unity3d/Klei/OxygenNot\\ Included/mods/local/ONIAgentBridge"
echo " mkdir -p \"\$MOD_DIR\""
echo " cp $OUTPUT_DIR/ONIAgentBridge.dll $MOD_DIR/mod_info.yaml \"\$MOD_DIR/\""
echo " # Then enable mod in ONI > Mods > ONI Agent Bridge"

295
scripts/event_daemon.py Normal file
View File

@ -0,0 +1,295 @@
#!/usr/bin/env python3
"""
ONI Agent Event Daemon
======================
Continuous event poller that feeds game events to the AI's input stream.
Architecture:
Game Mod --> Event Queue (via HTTP) --> Event Daemon --> AI Input Stream
The daemon:
1. Polls GET /api/state/events?since=<seq> every N seconds
2. Classifies events by severity (critical/warning/info)
3. For critical events: immediately triggers full analysis + prints alert
4. For warning events: logs and optionally triggers targeted checks
5. For info events: accumulates and reports periodically
6. Maintains a compact event log for AI context
"""
import json
import os
import sys
import time
import datetime
# Add tools to path
TOOLS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'tools')
sys.path.insert(0, TOOLS_DIR)
# Disable auto-pause for event daemon — we poll frequently and should not pause the game
import oni_api
oni_api.AUTO_PAUSE_ENABLED = False
from oni_api import api_get, api_post, api_url
# ── Configuration ──────────────────────────────────────────────────────────
POLL_INTERVAL = 5 # seconds between event polls
CRITICAL_POLL_INTERVAL = 2 # poll faster when critical events detected
MAX_EVENT_HISTORY = 200 # events kept in rolling buffer
CRITICAL_SEVERITIES = {'critical', 'duplicantdeath', 'buildingdamage', 'poweroutage'}
WARNING_SEVERITIES = {'warning', 'duplicantstress', 'lowoxygen', 'foodshortage'}
# ── Event History ──────────────────────────────────────────────────────────
class EventHistory:
"""Rolling buffer of events + statistics for AI context."""
def __init__(self, maxlen=MAX_EVENT_HISTORY):
self.events = []
self.maxlen = maxlen
self.stats = {
'total': 0,
'critical': 0,
'warning': 0,
'info': 0,
'by_category': {},
'by_type': {},
'last_poll_cycle': 0,
}
def push(self, events):
for e in events:
self.events.append(e)
self.stats['total'] += 1
sev = (e.get('severity') or 'info').lower()
cat = e.get('category', 'unknown')
etype = e.get('type', 'unknown')
if sev in ('critical', 'duplicantdeath', 'buildingdamage'):
self.stats['critical'] += 1
elif sev in ('warning',):
self.stats['warning'] += 1
else:
self.stats['info'] += 1
self.stats['by_category'][cat] = self.stats['by_category'].get(cat, 0) + 1
self.stats['by_type'][etype] = self.stats['by_type'].get(etype, 0) + 1
self.stats['last_poll_cycle'] = e.get('cycle', 0)
# Trim
if len(self.events) > self.maxlen:
self.events = self.events[-self.maxlen:]
def get_summary(self):
return {
'total_events': self.stats['total'],
'critical_count': self.stats['critical'],
'warning_count': self.stats['warning'],
'info_count': self.stats['info'],
'categories': dict(sorted(self.stats['by_category'].items(),
key=lambda x: -x[1])[:10]),
'last_cycle': self.stats['last_poll_cycle'],
'recent_critical': [e for e in self.events[-20:]
if (e.get('severity') or '').lower() in CRITICAL_SEVERITIES][-5:],
}
# ── Event Classifier ──────────────────────────────────────────────────────
def classify_event(e):
"""Return the action type for a given event."""
sev = (e.get('severity') or '').lower()
title = (e.get('title') or '').lower()
msg = (e.get('message') or '').lower()
cat = (e.get('category') or '').lower()
if sev in CRITICAL_SEVERITIES:
return 'critical'
if sev in WARNING_SEVERITIES:
return 'warning'
if cat == 'action':
return 'action_feedback'
# Content-based classification
combined = title + ' ' + msg
if any(w in combined for w in ['suffocat', 'choking', 'no oxygen', 'out of air']):
return 'critical'
if any(w in combined for w in ['starving', 'food', 'hungry']):
return 'warning'
if any(w in combined for w in ['heat', 'overheat', 'temperature', 'melt']):
return 'warning'
if any(w in combined for w in ['power', 'wattage', 'shutoff']):
return 'warning'
if any(w in combined for w in ['duplicant', 'stress', 'break']):
return 'warning'
if any(w in combined for w in ['research complete', 'research completed']):
return 'info_research'
if any(w in combined for w in ['printing pod']):
return 'info_printing_pod'
return 'info'
def format_event_for_ai(e):
"""Format an event as a structured string for AI input."""
ts = datetime.datetime.fromtimestamp(e.get('timestamp', time.time())).strftime('%H:%M:%S')
cycle = e.get('cycle', '?')
severity = e.get('severity', 'info').upper()
title = e.get('title', '?')
message = e.get('message', '')
lines = [f"[EVENT {severity}] Cycle {cycle} @ {ts}"]
lines.append(f" Title: {title}")
if message:
lines.append(f" Message: {message}")
entity = e.get('entity')
if entity:
lines.append(f" Entity: {entity}")
cell = e.get('cell')
if isinstance(cell, int) and cell >= 0:
lines.append(f" Cell: {cell}")
return '\n'.join(lines)
# ── Polling Loop ──────────────────────────────────────────────────────────
def poll_loop(event_history):
seq = 0
consecutive_errors = 0
print("[ONI Event Daemon] Starting event poll...")
print(f"[ONI Event Daemon] Poll interval: {POLL_INTERVAL}s")
print()
while True:
try:
data = api_get(f"/api/state/events?since={seq}&limit=50")
if 'error' in data:
consecutive_errors += 1
if consecutive_errors == 1:
print(f"[!] Cannot reach game: {data['error']}")
print(" Waiting for game connection...")
time.sleep(POLL_INTERVAL * 2)
continue
consecutive_errors = 0
events = data.get('events', [])
next_seq = data.get('next_seq', seq)
if events:
event_history.push(events)
# Classify and report
critical_events = []
for e in events:
cls = classify_event(e)
if cls == 'critical':
critical_events.append(e)
# Print alert with clear marker
print("=" * 56)
print(" *** CRITICAL EVENT ***")
print(format_event_for_ai(e))
print("=" * 56)
print()
# Auto-trigger full analysis on critical events
_trigger_emergency_analysis(e)
elif cls == 'warning':
print(format_event_for_ai(e))
print()
elif cls == 'info_research':
# Research completed — show with unlocks
print("=" * 40)
print(format_event_for_ai(e))
print(" -> Check what's new: python3 tools/oni_api.py buildable")
print("=" * 40)
print()
elif cls == 'info_printing_pod':
# Printing pod ready
print("=" * 40)
print(format_event_for_ai(e))
print(" -> View options: python3 tools/oni_api.py printing_pod")
print("=" * 40)
print()
else:
# Only print non-info events or batch feedback
cat = e.get('category', '')
if cat != 'general' or cls != 'info':
print(format_event_for_ai(e))
print()
# If critical events happened, poll faster for a bit
if critical_events:
seq = next_seq
time.sleep(CRITICAL_POLL_INTERVAL)
continue
seq = next_seq
time.sleep(POLL_INTERVAL)
except KeyboardInterrupt:
print("\n[ONI Event Daemon] Shutting down.")
summary = event_history.get_summary()
print(f" Total events seen: {summary['total_events']}")
print(f" Critical: {summary['critical_count']}, Warning: {summary['warning_count']}")
break
except Exception as e:
consecutive_errors += 1
if consecutive_errors <= 2:
print(f"[!] Poll error: {e}")
time.sleep(POLL_INTERVAL)
def _trigger_emergency_analysis(event):
"""On critical events, pull game state snapshot for AI context."""
try:
print(" -> Triggering emergency snapshot...")
game = api_get('/api/state/game')
alerts = api_get('/api/state/alert')
dups = api_get('/api/state/duplicants')
if 'error' not in game:
print(f" [SNAPSHOT] Cycle {game.get('cycle', '?')}, "
f"{game.get('duplicantCount', '?')} dupes, "
f"{game.get('suffocating', 0)} suffocating, "
f"{game.get('starving', 0)} starving, "
f"{game.get('stressed', 0)} stressed")
if isinstance(alerts, list) and alerts:
print(f" [ALERTS] {len(alerts)} active:")
for a in alerts[:3]:
print(f" - [{a.get('severity', '?')}] {a.get('title', '?')}")
print()
except:
pass
# ── Main ──────────────────────────────────────────────────────────────────
def main():
history = EventHistory()
try:
poll_loop(history)
except KeyboardInterrupt:
pass
# Print final summary
summary = history.get_summary()
print()
print("=" * 56)
print(" Event Daemon Session Summary")
print("=" * 56)
print(f" Total events: {summary['total_events']}")
print(f" Critical: {summary['critical_count']}")
print(f" Warning: {summary['warning_count']}")
print(f" Info: {summary['info_count']}")
print(f" Top categories: {', '.join(summary['categories'].keys())}")
print("=" * 56)
if __name__ == '__main__':
main()

185
scripts/fix_csharp.py Normal file
View File

@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Convert C# 7/8 features in ONIAgentBridge.cs to C# 6 compatible code.
DEPRECATED: The mod source (ONIAgentBridge.cs) has been rewritten to use if-else chains.
This script is kept for historical reference only and should not be run on the current source.
"""
import re
with open('mod/ONIAgentBridge.cs', 'r') as f:
content = f.read()
# 1. Convert tuple switch at ProcessRequest to if-else chain
# Find the switch and replace it
old_switch_start = """ switch (path, method)
{"""
# Read the entire switch block and reconstruct
lines = content.split('\n')
new_lines = []
i = 0
while i < len(lines):
line = lines[i]
# Find the C# 7 tuple switch
if 'switch (path, method)' in line and '{' in lines[i+1]:
new_lines.append(' // --- C# 6 compatible dispatch ---')
new_lines.append(' bool _handled = false;')
i += 2 # skip "switch (path, method)" and "{"
while i < len(lines):
stripped = lines[i].strip()
# Check for case pattern: case ("...", "..."):
m = re.match(r'case \("([^"]+)", "([^"]+)"\):', stripped)
if m:
endpoint = m.group(1)
method = m.group(2)
indent = lines[i][:len(lines[i]) - len(lines[i].lstrip())]
new_lines.append(f'{indent}else if (path == "{endpoint}" && method == "{method}")')
new_lines.append(f'{indent}{{')
new_lines.append(f'{indent} _handled = true;')
i += 1
# Copy lines until "break;"
while i < len(lines) and lines[i].strip() != 'break;':
new_lines.append(lines[i])
i += 1
new_lines.append(f'{indent}}}')
i += 1 # skip "break;"
continue
# Check for default case
if stripped == 'default:':
i += 1
# Skip the opening brace
if lines[i].strip() == '{':
i += 1
new_lines.append(' if (!_handled)')
new_lines.append(' {')
while i < len(lines):
if lines[i].strip() == '}' and not _is_end_of_method(lines, i):
# This could be the switch closing brace
# Check if next line is the method closing
break
new_lines.append(lines[i])
i += 1
new_lines.append(' }')
# Skip the closing brace of the switch
if lines[i].strip() == '}':
i += 1
continue
# Check for closing brace of switch
if stripped == '}':
i += 1
break
# Any other content inside switch (shouldn't happen)
new_lines.append(lines[i])
i += 1
continue
new_lines.append(line)
i += 1
content = '\n'.join(new_lines)
def _is_end_of_method(lines, idx):
"""Check if this } ends a method (next non-blank is a method or class member)."""
for j in range(idx+1, min(idx+10, len(lines))):
s = lines[j].strip()
if s == '' or s.startswith('//') or s.startswith('/*'):
continue
if s.startswith('private ') or s.startswith('public ') or s.startswith('internal ') or s.startswith('static ') or s.startswith('}'):
return True
return False
return False
# 2. Fix switch expressions (C# 8): `wireType switch { ... }` -> if-else
# Pattern: string buildingId = wireType switch { "heavy" => "HeaviWatWire", ... }
def fix_switch_expr(match):
full = match.group(0)
var_name = match.group(1)
# Parse the switch arms
# Extract the expression after "= "
body = match.group(2)
return full # placeholder, we'll handle below
# Fix: string buildingId = wireType switch { "heavy" => "HeaviWatWire", _ => "Wire" };
# Convert to: string buildingId; if (wireType == "heavy") buildingId = "HeaviWatWire"; else ...
for pattern in [
(r'wireType switch\s*\{([^}]+)\}', 'wireType'),
(r'error switch\s*\{([^}]+)\}', 'error'),
(r'i switch\s*\{([^}]+)\}', 'i'),
]:
pat, varname = pattern
matches = list(re.finditer(pat, content))
for m in reversed(matches):
body = m.group(1)
arms = re.findall(r'"([^"]*)"\s*=>\s*"([^"]*)"', body)
default = re.findall(r'_\s*=>\s*(null|"[^"]*")', body)
if varname == 'i':
# label assignment
replacements = []
for key, val in arms:
replacements.append(f' if ({varname} == {key}) label = "{val}";')
if default:
replacements.append(f' else label = {default[0]};')
new_code = '\n'.join(replacements)
else:
# Find the variable being assigned
start = m.start()
# Look backwards to find the variable name
line_start = content.rfind('\n', 0, start) + 1
line_before = content[line_start:start]
var_match = re.match(r'\s*(?:\w+\s+)?(\w+)\s*=\s*$', line_before)
indent = ' ' * 16
if varname == 'wireType':
# Multiple instances in wire building, find each by context
context_before = content[max(0,start-200):start]
if 'bridgeId' in context_before:
varname_full = 'bridgeId'
else:
varname_full = 'buildingId'
else:
varname_full = varname
lines_code = [f' string {varname_full};']
first = True
for key, val in arms:
prefix = 'if' if first else 'else if'
lines_code.append(f' {prefix} ({varname} == "{key}") {varname_full} = "{val}";')
first = False
if default:
dval = default[0]
lines_code.append(f' else {varname_full} = {dval};')
new_code = '\n'.join(lines_code)
# Replace the assignment line + switch expression
# Find the full assignment
assign_start = content.rfind('\n', 0, start)
assign_end = m.end()
while assign_end < len(content) and content[assign_end] != '\n':
assign_end += 1
old = content[assign_start:assign_end]
content = content[:assign_start] + '\n' + new_code + content[assign_end:]
# Write result
with open('mod/ONIAgentBridge.cs', 'w') as f:
f.write(content)
print('Done. Checking for remaining C# 7+ features...')
# Verify
remaining = re.findall(r'\bswitch\b', content)
print(f'Remaining "switch" keywords: {len(remaining)}')
# Count them
for i, line in enumerate(content.split('\n')):
if 'switch' in line and not line.strip().startswith('//') and 'ProcessRequest' not in line:
sline = line.strip()
if 'if (' not in line and 'dictionary' not in line.lower() and 'dispatch' not in line:
print(f' Line ~{i}: {sline[:80]}')

28
scripts/setup.sh Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# ONI Agent - Project Setup Script
# Ensures Python dependencies and directory structure.
set -euo pipefail
echo "[ONI Agent] Setting up project..."
# Verify Python3
if ! command -v python3 &> /dev/null; then
echo "[!] Python3 is required but not found."
exit 1
fi
echo "[OK] Python3 found: $(python3 --version)"
# Verify Python tools are syntactically valid
TOOLS_DIR="$(dirname "$0")/../tools"
for f in "$TOOLS_DIR"/*.py; do
python3 -m py_compile "$f" 2>/dev/null && echo "[OK] $f" || echo "[!] Syntax error in $f"
done
echo ""
echo "[OK] Setup complete."
echo ""
echo "Next steps:"
echo " 1. Launch Oxygen Not Included with the ONI Agent Bridge mod"
echo " 2. Run: python3 tools/oni_api.py health"
echo " 3. Run: python3 tools/oni_api.py status"
echo " 4. Run: python3 tools/oni_analyzer.py"

26
scripts/watch.sh Normal file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# ONI Agent - Watch Mode
# Continuously polls game state and runs analysis on a timer.
set -euo pipefail
SCRIPT_DIR="$(dirname "$0")"
TOOLS_DIR="$SCRIPT_DIR/../tools"
INTERVAL=${1:-60}
if ! [[ "$INTERVAL" =~ ^[0-9]+$ ]] || [ "$INTERVAL" -lt 10 ]; then
echo "Usage: watch.sh [interval_seconds] (minimum 10)"
exit 1
fi
echo "[ONI Agent] Watch mode started (interval: ${INTERVAL}s)"
echo "Press Ctrl+C to stop."
echo ""
while true; do
clear 2>/dev/null || true
output=$(python3 "$TOOLS_DIR/oni_analyzer.py" 2>&1)
echo "$output"
echo ""
echo "[Next update in ${INTERVAL}s...]"
sleep "$INTERVAL"
done