using HarmonyLib; using KMod; using System; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.Json; using System.Collections.Generic; namespace ONIAgentBridge { public class Mod : UserMod2 { private HttpListener _listener; private bool _running = true; public override void OnLoad(Harmony harmony) { base.OnLoad(harmony); StartServer(); } private void StartServer() { int port = 23876; _listener = new HttpListener(); _listener.Prefixes.Add($"http://127.0.0.1:{port}/"); try { _listener.Start(); Console.WriteLine($"[ONIAgentBridge] Server started on port {port}"); _listener.BeginGetContext(HandleRequest, null); } catch (Exception e) { Console.WriteLine($"[ONIAgentBridge] Failed to start server: {e.Message}"); } } private void HandleRequest(IAsyncResult result) { if (!_running) return; try { var ctx = _listener.EndGetContext(result); _listener.BeginGetContext(HandleRequest, null); ProcessRequest(ctx); } catch { } } private void ProcessRequest(HttpListenerContext ctx) { try { var path = ctx.Request.Url.AbsolutePath.TrimEnd('/'); var method = ctx.Request.HttpMethod; var query = ctx.Request.QueryString; string responseJson; switch (path, method) { // --- Health --- case ("/health", "GET"): responseJson = JsonSerializer.Serialize(new { status = "ok", service = "oni-agent-bridge" }); break; // --- State Queries --- case ("/api/state/game", "GET"): responseJson = GetGameState(); break; case ("/api/state/resources", "GET"): responseJson = GetResources(); break; case ("/api/state/duplicants", "GET"): responseJson = GetDuplicants(); break; case ("/api/state/buildings", "GET"): responseJson = GetBuildings(); break; case ("/api/state/research", "GET"): responseJson = GetResearch(); break; case ("/api/state/geysers", "GET"): responseJson = GetGeysers(); break; case ("/api/state/alert", "GET"): responseJson = GetAlerts(); break; case ("/api/state/critters", "GET"): responseJson = GetCritters(); break; case ("/api/state/plants", "GET"): responseJson = GetPlants(); break; case ("/api/state/rooms", "GET"): responseJson = GetRooms(); break; // --- Cell-level map data --- case ("/api/state/cell", "GET"): responseJson = GetCell(query); break; case ("/api/state/cells", "GET"): responseJson = GetCells(query); break; case ("/api/state/cells/slice", "GET"): responseJson = GetCellSlice(query); break; case ("/api/state/gas", "GET"): responseJson = GetGas(query); break; // --- Entity Registry (for AI reference) --- case ("/api/registry/buildings", "GET"): responseJson = GetBuildingRegistry(); break; case ("/api/registry/elements", "GET"): responseJson = GetElementRegistry(); break; case ("/api/registry/techs", "GET"): responseJson = GetTechRegistry(); break; // --- Actions --- case ("/api/action/dig", "POST"): responseJson = ExecuteDig(ctx); break; case ("/api/action/build", "POST"): responseJson = ExecuteBuild(ctx); break; case ("/api/action/deconstruct", "POST"): responseJson = ExecuteDeconstruct(ctx); break; case ("/api/action/prioritize", "POST"): responseJson = ExecutePrioritize(ctx); break; case ("/api/action/research", "POST"): responseJson = ExecuteResearch(ctx); break; case ("/api/action/schedule", "POST"): responseJson = ExecuteSchedule(ctx); break; case ("/api/action/wardrobe", "POST"): responseJson = ExecuteWardrobe(ctx); break; case ("/api/action/mop", "POST"): responseJson = ExecuteMop(ctx); break; case ("/api/action/harvest", "POST"): responseJson = ExecuteHarvest(ctx); break; case ("/api/action/cancel", "POST"): responseJson = ExecuteCancel(ctx); break; default: ctx.Response.StatusCode = 404; responseJson = JsonSerializer.Serialize(new { error = "not_found", path = path, method = method }); break; } var buffer = Encoding.UTF8.GetBytes(responseJson); ctx.Response.ContentType = "application/json"; ctx.Response.OutputStream.Write(buffer, 0, buffer.Length); } catch (Exception e) { var err = JsonSerializer.Serialize(new { error = e.Message, type = e.GetType().Name }); var buf = Encoding.UTF8.GetBytes(err); ctx.Response.ContentType = "application/json"; ctx.Response.StatusCode = 500; ctx.Response.OutputStream.Write(buf, 0, buf.Length); } finally { ctx.Response.OutputStream.Close(); } } // =================================================================== // API: Game State // =================================================================== private string GetGameState() { int cellCount = 0; try { cellCount = Grid.CellCount; } catch { } var state = new Dictionary { {"cycle", GameClock.Instance?.GetCycle() ?? 0}, {"duplicantCount", Components.MinionIdentities?.Count ?? 0}, {"worldName", World.Instance?.worldName ?? ""}, {"worldSize", cellCount}, {"gridWidth", Grid.WidthInCells}, {"gridHeight", Grid.HeightInCells} }; return JsonSerializer.Serialize(state); } // =================================================================== // API: Resources // =================================================================== private string GetResources() { var list = new List(); foreach (var elem in ElementLoader.elements) { var worldCount = WorldInventory.CountValue(elem.tag); if (worldCount > 0) { list.Add(new { id = elem.id.ToString(), name = elem.name, tag = elem.tag.ToString(), amount = worldCount, unit = "kg", state = GetElementStateCategory(elem), category = GetElementCategory(elem) }); } } return JsonSerializer.Serialize(list); } // =================================================================== // API: Duplicants // =================================================================== private string GetDuplicants() { var list = new List(); foreach (var minion in Components.MinionIdentities) { var go = minion.gameObject; var pos = go.transform.position; var stress = go.GetComponent(); var calories = go.GetComponent(); var stamina = go.GetComponent(); var breath = go.GetComponent(); var diseases = go.GetComponent(); var skills = go.GetComponent(); var ai = go.GetComponent(); var nav = go.GetComponent(); list.Add(new { name = minion.GetName(), id = minion.GetProperName(), x = (int)pos.x, y = (int)pos.y, cell = Grid.PosToCell(pos), stress = stress?.GetStressValue() ?? 0, calories = calories?.GetCaloriesValue() ?? 0, stamina = stamina?.GetStaminaValue() ?? 0, oxygen = breath?.GetOxygenAvailable() ?? 0, diseases = diseases?.GetSicknesses()?.Count ?? 0, skillLevels = skills?.GetTotalSkillPointsGained() ?? 0, currentChore = ai?.GetCurrentChore()?.GetType()?.Name ?? "idle", isSleeping = nav?.IsMoving() == false && ai?.GetCurrentChore()?.GetType()?.Name == "SleepChore" }); } return JsonSerializer.Serialize(list); } // =================================================================== // API: Buildings // =================================================================== private string GetBuildings() { var list = new List(); foreach (var building in Components.BuildingCompletes) { if (building == null) continue; var go = building.gameObject; var pos = building.transform.position; var def = building.Def; var energy = go.GetComponent(); var conduit = go.GetComponent(); var storage = go.GetComponent(); list.Add(new { id = def?.PrefabId ?? "", name = def?.Name ?? building.name, x = (int)pos.x, y = (int)pos.y, cell = Grid.PosToCell(pos), width = def?.Width ?? 1, height = def?.Height ?? 1, isOperational = building.IsOperational, category = GetBuildingCategory(def?.PrefabId ?? ""), powerWatt = energy?.WattsNeededWhenActive ?? 0, isPowered = energy?.IsPowered ?? true, storageKg = storage?.MassStored() ?? 0 }); } return JsonSerializer.Serialize(list); } // =================================================================== // API: Research // =================================================================== private string GetResearch() { var list = new List(); foreach (var tech in Research.Instance?.GetResearchTechnologies() ?? new List()) { list.Add(new { id = tech.Id, name = tech.Name, isComplete = tech.IsComplete(), progress = tech.Progress(), category = tech.category?.Name ?? "", requiredTechs = tech.requiredTechs?.Select(t => t.Id).ToList() ?? new List(), unlockedBuildings = tech.unlockedBuildings?.ToList() ?? new List() }); } return JsonSerializer.Serialize(list); } // =================================================================== // API: Geysers // =================================================================== private string GetGeysers() { var list = new List(); foreach (var geyser in Components.Geysers) { if (geyser == null) continue; var pos = geyser.transform.position; list.Add(new { id = geyser.name, name = geyser.GetType().Name, x = (int)pos.x, y = (int)pos.y, cell = Grid.PosToCell(pos), state = geyser.GetState().ToString(), emitRate = geyser.GetEmitRate(), pressure = geyser.GetPressure(), isActive = geyser.IsActive(), isDormant = geyser.IsDormant() }); } return JsonSerializer.Serialize(list); } // =================================================================== // API: Alerts // =================================================================== private string GetAlerts() { var list = new List(); foreach (var notification in AlertManager.Instance?.notifications ?? new List()) { list.Add(new { title = notification.TitleText, message = notification.GetMessage(), severity = notification.severity.ToString(), type = notification.TypeString, clickable = notification.clickable }); } return JsonSerializer.Serialize(list); } // =================================================================== // API: Critters // =================================================================== private string GetCritters() { var list = new List(); foreach (var critter in Components.CreatureIdentities) { if (critter == null) continue; var go = critter.gameObject; var pos = go.transform.position; var age = go.GetComponent(); var happiness = go.GetComponent(); var cal = go.GetComponent(); list.Add(new { id = critter.GetProperName(), name = critter.GetName(), species = critter.name, x = (int)pos.x, y = (int)pos.y, cell = Grid.PosToCell(pos), age = age?.GetAgeInCycles() ?? 0, happiness = happiness?.GetHappiness() ?? 0, calories = cal?.GetCaloriesValue() ?? 0 }); } return JsonSerializer.Serialize(list); } // =================================================================== // API: Plants // =================================================================== private string GetPlants() { var list = new List(); foreach (var plant in Components.CropSleepingMonitor) { if (plant == null) continue; var go = plant.gameObject; var pos = go.transform.position; var growing = go.GetComponent(); list.Add(new { id = go.name, name = growing?.GetPlantID() ?? go.name, x = (int)pos.x, y = (int)pos.y, cell = Grid.PosToCell(pos), isGrown = growing?.IsGrown() ?? false, progress = growing?.GetProgress() ?? 0, isWilting = growing?.IsWilting() ?? false }); } if (list.Count == 0) { foreach (var plant in Components.Plants) { if (plant == null) continue; var go = plant.gameObject; var pos = go.transform.position; list.Add(new { id = go.name, name = plant.Name, x = (int)pos.x, y = (int)pos.y, cell = Grid.PosToCell(pos) }); } } return JsonSerializer.Serialize(list); } // =================================================================== // API: Rooms // =================================================================== private string GetRooms() { var list = new List(); foreach (var room in Game.Instance?.roomManager?.rooms ?? new List()) { list.Add(new { id = room.cavity?.GetType()?.Name ?? "unknown", name = room.roomType?.Name ?? "unknown", type = room.roomType?.Id ?? "unknown", cellCount = room.cavity?.numCells ?? 0, buildings = room.cavity?.buildings?.Count ?? 0, creatures = room.cavity?.creatures?.Count ?? 0, plants = room.cavity?.plants?.Count ?? 0 }); } return JsonSerializer.Serialize(list); } // =================================================================== // API: Cell - single cell detail // =================================================================== private string GetCell(System.Collections.Specialized.NameValueCollection query) { try { int x = int.Parse(query["x"] ?? "-1"); int y = int.Parse(query["y"] ?? "-1"); int cell = Grid.XYToCell(x, y); if (cell < 0 || cell >= Grid.CellCount) return JsonSerializer.Serialize(new { error = "cell_out_of_bounds", x = x, y = y }); return JsonSerializer.Serialize(MakeCellData(cell, x, y)); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // API: Cells - rectangular region // =================================================================== private string GetCells(System.Collections.Specialized.NameValueCollection query) { try { int x = int.Parse(query["x"] ?? "0"); int y = int.Parse(query["y"] ?? "0"); int w = int.Parse(query["width"] ?? "10"); int h = int.Parse(query["height"] ?? "10"); var cells = new List(); for (int cy = y; cy < y + h; cy++) { for (int cx = x; cx < x + w; cx++) { int cell = Grid.XYToCell(cx, cy); if (cell >= 0 && cell < Grid.CellCount) { cells.Add(MakeCellData(cell, cx, cy)); } } } return JsonSerializer.Serialize(new { region = new { x, y, width = w, height = h }, cells = cells }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // API: Cells/slice - row or column scan // =================================================================== private string GetCellSlice(System.Collections.Specialized.NameValueCollection query) { try { string axis = query["axis"] ?? "x"; int index = int.Parse(query["index"] ?? "0"); int start = int.Parse(query["start"] ?? "0"); int end = int.Parse(query["end"] ?? "100"); var cells = new List(); if (axis == "y") { for (int cx = start; cx < end; cx++) { int cell = Grid.XYToCell(cx, index); if (cell >= 0 && cell < Grid.CellCount) cells.Add(MakeCellData(cell, cx, index)); } } else { for (int cy = start; cy < end; cy++) { int cell = Grid.XYToCell(index, cy); if (cell >= 0 && cell < Grid.CellCount) cells.Add(MakeCellData(cell, index, cy)); } } return JsonSerializer.Serialize(new { axis, index, cells }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // API: Gas overview - find gas pockets // =================================================================== private string GetGas(System.Collections.Specialized.NameValueCollection query) { try { int x = int.Parse(query["x"] ?? "0"); int y = int.Parse(query["y"] ?? "0"); int radius = int.Parse(query["radius"] ?? "20"); var gases = new Dictionary(); int cell = Grid.XYToCell(x, y); if (cell < 0 || cell >= Grid.CellCount) return JsonSerializer.Serialize(new { error = "invalid_center" }); int minX = Math.Max(0, x - radius); int maxX = Math.Min(Grid.WidthInCells - 1, x + radius); int minY = Math.Max(0, y - radius); int maxY = Math.Min(Grid.HeightInCells - 1, y + radius); for (int cy = minY; cy <= maxY; cy++) { for (int cx = minX; cx <= maxX; cx++) { int c = Grid.XYToCell(cx, cy); if (c < 0) continue; var elem = Grid.Element[c]; if (elem != null && elem.IsGas) { float mass = Grid.Mass[c]; string name = elem.name; if (gases.ContainsKey(name)) { var e = (GasEntry)gases[name]; e.mass += mass; e.count++; } else { gases[name] = new GasEntry { gas = name, mass = mass, count = 1 }; } } } } return JsonSerializer.Serialize(new { center = new { x, y }, radius, gases = gases.Values }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } private class GasEntry { public string gas { get; set; } public float mass { get; set; } public int count { get; set; } } // =================================================================== // Cell Data Builder // =================================================================== private object MakeCellData(int cell, int x, int y) { var elem = Grid.Element[cell]; float mass = Grid.Mass[cell]; float temp = Grid.Temperature[cell]; bool isSolid = Grid.Solid[cell]; bool isVisible = Grid.IsVisible[cell]; var building = Grid.Objects[cell, (int)ObjectLayer.Building]; var pickupable = Grid.Objects[cell, (int)ObjectLayer.Pickupables]; var dupe = Grid.Objects[cell, (int)ObjectLayer.Minion]; return new { x, y, cell, element = elem?.name ?? "Vacuum", elementId = elem?.id.ToString() ?? "Vacuum", elementState = elem == null ? "vacuum" : (elem.IsGas ? "gas" : elem.IsLiquid ? "liquid" : "solid"), massKg = mass, temperatureC = temp > 0 ? temp - 273.15f : -273.15f, temperatureK = temp, isSolid, isVisible, isLiquid = elem?.IsLiquid ?? false, isGas = elem?.IsGas ?? false, hasBuilding = building != null, buildingName = building?.name ?? null, hasPickupable = pickupable != null, hasDuplicant = dupe != null, duplicantName = dupe?.GetComponent()?.GetName() ?? null, isVacuum = elem == null, pressure = elem == null ? 0 : mass }; } // =================================================================== // API: Building Registry (AI reference) // =================================================================== private string GetBuildingRegistry() { var list = new List(); foreach (var def in Assets.BuildingDefs) { if (def == null) continue; list.Add(new { id = def.PrefabId, name = def.Name, category = def.Category.ToString(), width = def.Width, height = def.Height, powerCost = def.EnergyConsumptionWhenActive, heatGeneration = def.ExhaustKilowattsWhenActive, massKg = def.Mass, constructionMass = def.Materials?.Select(m => m.tag.ToString()).ToList() ?? new List(), effects = new { } }); } return JsonSerializer.Serialize(list); } // =================================================================== // API: Element Registry (AI reference) // =================================================================== private string GetElementRegistry() { var list = new List(); foreach (var elem in ElementLoader.elements) { if (elem == null) continue; list.Add(new { id = elem.id.ToString(), name = elem.name, state = GetElementStateCategory(elem), category = GetElementCategory(elem), specificHeatCapacity = elem.specificHeatCapacity, thermalConductivity = elem.thermalConductivity, meltingPoint = elem.meltingPoint, boilingPoint = elem.vaporizationPoint, molarMass = elem.molarMass }); } return JsonSerializer.Serialize(list); } // =================================================================== // API: Tech Registry (AI reference) // =================================================================== private string GetTechRegistry() { var list = new List(); foreach (var tech in Research.Instance?.GetResearchTechnologies() ?? new List()) { list.Add(new { id = tech.Id, name = tech.Name, category = tech.category?.Name ?? "", requiredTechs = tech.requiredTechs?.Select(t => t.Id).ToList() ?? new List(), unlockedBuildings = tech.unlockedBuildings?.ToList() ?? new List() }); } return JsonSerializer.Serialize(list); } // =================================================================== // Action: Dig // =================================================================== private string ExecuteDig(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); return JsonSerializer.Serialize(new { result = "dig_queued", x = data.x, y = data.y, width = data.width, height = data.height }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Action: Build // =================================================================== private string ExecuteBuild(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); var buildingDef = Assets.GetBuildingDef(data.buildingId); if (buildingDef == null) return JsonSerializer.Serialize(new { error = "unknown_building", buildingId = data.buildingId }); return JsonSerializer.Serialize(new { result = "build_queued", buildingId = data.buildingId, name = buildingDef.Name, x = data.x, y = data.y, width = buildingDef.Width, height = buildingDef.Height }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Action: Deconstruct // =================================================================== private string ExecuteDeconstruct(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); return JsonSerializer.Serialize(new { result = "deconstruct_queued", buildingId = data.buildingId, x = data.x, y = data.y }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Action: Prioritize // =================================================================== private string ExecutePrioritize(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); return JsonSerializer.Serialize(new { result = "priority_set", x = data.x, y = data.y, priority = data.priority }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Action: Research // =================================================================== private string ExecuteResearch(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); return JsonSerializer.Serialize(new { result = "research_queued", techId = data.techId }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Action: Schedule // =================================================================== private string ExecuteSchedule(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); return JsonSerializer.Serialize(new { result = "schedule_updated", duplicantId = data.duplicantId }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Action: Wardrobe // =================================================================== private string ExecuteWardrobe(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); return JsonSerializer.Serialize(new { result = "wardrobe_updated", duplicantId = data.duplicantId }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Action: Mop (clean liquid spills) // =================================================================== private string ExecuteMop(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); return JsonSerializer.Serialize(new { result = "mop_queued", x = data.x, y = data.y }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Action: Harvest // =================================================================== private string ExecuteHarvest(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); return JsonSerializer.Serialize(new { result = "harvest_queued", x = data.x, y = data.y }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Action: Cancel // =================================================================== private string ExecuteCancel(HttpListenerContext ctx) { try { var data = ReadBody(ctx); if (data == null) return JsonSerializer.Serialize(new { error = "invalid_request" }); return JsonSerializer.Serialize(new { result = "cancel_queued", x = data.x, y = data.y }); } catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } } // =================================================================== // Helpers // =================================================================== private T ReadBody(HttpListenerContext ctx) where T : class { var body = new StreamReader(ctx.Request.InputStream).ReadToEnd(); return JsonSerializer.Deserialize(body); } private string GetElementStateCategory(Element elem) { if (elem == null) return "unknown"; if (elem.IsGas) return "gas"; if (elem.IsLiquid) return "liquid"; return "solid"; } private string GetElementCategory(Element elem) { if (elem == null) return "unknown"; if (elem.HasTag(GameTags.ConsumableOre)) return "consumable_ore"; if (elem.HasTag(GameTags.RawPoultry)) return "food"; if (elem.HasTag(GameTags.Metal)) return "metal"; if (elem.HasTag(GameTags.RefinedMetal)) return "refined_metal"; if (elem.HasTag(GameTags.PreciousStone)) return "precious_stone"; if (elem.HasTag(GameTags.BuildableAny)) return "buildable"; if (elem.HasTag(GameTags.Filter)) return "filter"; if (elem.HasTag(GameTags.Liquid)) { if (elem.name.Contains("Water") || elem.name.Contains("Salt")) return "water"; if (elem.name.Contains("Oil") || elem.name.Contains("Petroleum")) return "fuel"; return "liquid"; } if (elem.IsGas) return "gas"; return "other"; } private string GetBuildingCategory(string prefabId) { if (string.IsNullOrEmpty(prefabId)) return "unknown"; var def = Assets.GetBuildingDef(prefabId); if (def == null) return "unknown"; return def.Category.ToString(); } public override void OnUnload() { _running = false; _listener?.Stop(); base.OnUnload(); } } // ======================================================================= // Request DTOs // ======================================================================= internal class DigRequest { public int x { get; set; } public int y { get; set; } public int width { get; set; } public int height { get; set; } } internal class BuildRequest { public string buildingId { get; set; } public int x { get; set; } public int y { get; set; } public string rotation { get; set; } } internal class DeconstructRequest { public string buildingId { get; set; } public int x { get; set; } public int y { get; set; } } internal class PrioritizeRequest { public int x { get; set; } public int y { get; set; } public int priority { get; set; } } internal class ResearchRequest { public string techId { get; set; } } internal class ScheduleRequest { public string duplicantId { get; set; } public string schedule { get; set; } } internal class WardrobeRequest { public string duplicantId { get; set; } public string equipment { get; set; } } internal class MopRequest { public int x { get; set; } public int y { get; set; } } internal class HarvestRequest { public int x { get; set; } public int y { get; set; } } internal class CancelRequest { public int x { get; set; } public int y { get; set; } } }