ONI Agent Bridge - initial mod implementation
This commit is contained in:
142
scripts/annotate_apis.py
Normal file
142
scripts/annotate_apis.py
Normal 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
30
scripts/auto_analyze.sh
Normal 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
28
scripts/auto_repair.sh
Normal 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
61
scripts/build_mod.sh
Normal 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
295
scripts/event_daemon.py
Normal 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
185
scripts/fix_csharp.py
Normal 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
28
scripts/setup.sh
Normal 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
26
scripts/watch.sh
Normal 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
|
||||
Reference in New Issue
Block a user