diff --git a/client/demo.js b/client/demo.js index 0dfe0aee..2f05382f 100644 --- a/client/demo.js +++ b/client/demo.js @@ -112,6 +112,8 @@ class DemoDataGenerator { app.get('/demo/bullseyes', (req, res) => this.bullseyes(req, res)); app.get('/demo/airbases', (req, res) => this.airbases(req, res)); app.get('/demo/mission', (req, res) => this.mission(req, res)); + app.get('/demo/commands', (req, res) => this.command(req, res)); + app.put('/demo', (req, res) => this.put(req, res)); app.use('/demo', basicAuth({ users: { @@ -457,7 +459,17 @@ class DemoDataGenerator { } res.send(JSON.stringify(ret)); } + + command(req, res) { + var ret = {commandExecuted: Math.random() > 0.5}; + res.send(JSON.stringify(ret)); + } + put(req, res) { + var ret = {commandHash: Math.random().toString(36).slice(2, 19)} + res.send(JSON.stringify(ret)); + } + } module.exports = DemoDataGenerator; \ No newline at end of file diff --git a/client/src/@types/server.d.ts b/client/src/@types/server.d.ts index 96701f8f..851bfd53 100644 --- a/client/src/@types/server.d.ts +++ b/client/src/@types/server.d.ts @@ -44,4 +44,9 @@ interface LogData { logs: {[key: string]: string}, sessionHash: string; time: number; +} + +interface ServerRequestOptions { + time?: number; + commandHash?: string; } \ No newline at end of file diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index 1013f6e3..d1124434 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -1,5 +1,13 @@ import { LatLng, LatLngBounds } from "leaflet"; +export const UNITS_URI = "units"; +export const WEAPONS_URI = "weapons"; +export const LOGS_URI = "logs"; +export const AIRBASES_URI = "airbases"; +export const BULLSEYE_URI = "bullseyes"; +export const MISSION_URI = "mission"; +export const COMMANDS_URI = "commands"; + export const NONE = "None"; export const GAME_MASTER = "Game master"; export const BLUE_COMMANDER = "Blue commander"; diff --git a/client/src/contextmenus/airbasecontextmenu.ts b/client/src/contextmenus/airbasecontextmenu.ts index 526b41da..a6304d42 100644 --- a/client/src/contextmenus/airbasecontextmenu.ts +++ b/client/src/contextmenus/airbasecontextmenu.ts @@ -2,6 +2,7 @@ import { getMap, getMissionHandler, getUnitsManager, setActiveCoalition } from " import { GAME_MASTER } from "../constants/constants"; import { Airbase } from "../mission/airbase"; import { dataPointMap } from "../other/utils"; +import { Unit } from "../unit/unit"; import { ContextMenu } from "./contextmenu"; /** This context menu is shown to the user when the airbase marker is right clicked on the map. @@ -38,7 +39,7 @@ export class AirbaseContextMenu extends ContextMenu { this.#setProperties(this.#airbase.getProperties()); this.#setParkings(this.#airbase.getParkings()); this.#setCoalition(this.#airbase.getCoalition()); - this.#showLandButton(getUnitsManager().getSelectedUnitsTypes().length == 1 && ["Aircraft", "Helicopter"].includes(getUnitsManager().getSelectedUnitsTypes()[0]) && (getUnitsManager().getSelectedUnitsCoalition() === this.#airbase.getCoalition() || this.#airbase.getCoalition() === "neutral")) + this.#showLandButton(getUnitsManager().getSelectedUnitsTypes().length == 1 && ["Aircraft", "Helicopter"].includes(getUnitsManager().getSelectedUnitsTypes()[0]) && (getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getCoalition()}) === this.#airbase.getCoalition() || this.#airbase.getCoalition() === "neutral")) this.#showSpawnButton(getMissionHandler().getCommandModeOptions().commandMode == GAME_MASTER || this.#airbase.getCoalition() == getMissionHandler().getCommandedCoalition()); this.#setAirbaseData(); diff --git a/client/src/controls/unitspawnmenu.ts b/client/src/controls/unitspawnmenu.ts index 16096d8c..5a6a64cc 100644 --- a/client/src/controls/unitspawnmenu.ts +++ b/client/src/controls/unitspawnmenu.ts @@ -412,10 +412,13 @@ export class AircraftSpawnMenu extends UnitSpawnMenu { for (let i = 1; i < unitsCount + 1; i++) { units.push(unitTable); } - if (getUnitsManager().spawnUnits("Aircraft", units, getActiveCoalition(), false, spawnOptions.airbase ? spawnOptions.airbase.getName() : "", spawnOptions.country)) { - getMap().addTemporaryMarker(spawnOptions.latlng, spawnOptions.name, getActiveCoalition()); - getMap().getMapContextMenu().hide(); - } + + getUnitsManager().spawnUnits("Aircraft", units, getActiveCoalition(), false, spawnOptions.airbase ? spawnOptions.airbase.getName() : "", spawnOptions.country, (res: any) => { + if (res.commandHash !== undefined) + getMap().addTemporaryMarker(spawnOptions.latlng, spawnOptions.name, getActiveCoalition(), res.commandHash); + }); + + getMap().getMapContextMenu().hide(); } } } @@ -447,10 +450,13 @@ export class HelicopterSpawnMenu extends UnitSpawnMenu { for (let i = 1; i < unitsCount + 1; i++) { units.push(unitTable); } - if (getUnitsManager().spawnUnits("Helicopter", units, getActiveCoalition(), false, spawnOptions.airbase ? spawnOptions.airbase.getName() : "", spawnOptions.country)) { - getMap().addTemporaryMarker(spawnOptions.latlng, spawnOptions.name, getActiveCoalition()); - getMap().getMapContextMenu().hide(); - } + + getUnitsManager().spawnUnits("Helicopter", units, getActiveCoalition(), false, spawnOptions.airbase ? spawnOptions.airbase.getName() : "", spawnOptions.country, (res: any) => { + if (res.commandHash !== undefined) + getMap().addTemporaryMarker(spawnOptions.latlng, spawnOptions.name, getActiveCoalition(), res.commandHash); + }); + + getMap().getMapContextMenu().hide(); } } } @@ -481,10 +487,13 @@ export class GroundUnitSpawnMenu extends UnitSpawnMenu { units.push(JSON.parse(JSON.stringify(unitTable))); unitTable.location.lat += 0.0001; } - if (getUnitsManager().spawnUnits("GroundUnit", units, getActiveCoalition(), false, spawnOptions.airbase ? spawnOptions.airbase.getName() : "", spawnOptions.country)) { - getMap().addTemporaryMarker(spawnOptions.latlng, spawnOptions.name, getActiveCoalition()); - getMap().getMapContextMenu().hide(); - } + + getUnitsManager().spawnUnits("GroundUnit", units, getActiveCoalition(), false, spawnOptions.airbase ? spawnOptions.airbase.getName() : "", spawnOptions.country, (res: any) => { + if (res.commandHash !== undefined) + getMap().addTemporaryMarker(spawnOptions.latlng, spawnOptions.name, getActiveCoalition(), res.commandHash); + }); + + getMap().getMapContextMenu().hide(); } } } @@ -515,10 +524,13 @@ export class NavyUnitSpawnMenu extends UnitSpawnMenu { units.push(JSON.parse(JSON.stringify(unitTable))); unitTable.location.lat += 0.0001; } - if (getUnitsManager().spawnUnits("NavyUnit", units, getActiveCoalition(), false, spawnOptions.airbase ? spawnOptions.airbase.getName() : "", spawnOptions.country)) { - getMap().addTemporaryMarker(spawnOptions.latlng, spawnOptions.name, getActiveCoalition()); - getMap().getMapContextMenu().hide(); - } + + getUnitsManager().spawnUnits("NavyUnit", units, getActiveCoalition(), false, spawnOptions.airbase ? spawnOptions.airbase.getName() : "", spawnOptions.country, (res: any) => { + if (res.commandHash !== undefined) + getMap().addTemporaryMarker(spawnOptions.latlng, spawnOptions.name, getActiveCoalition(), res.commandHash); + }); + + getMap().getMapContextMenu().hide(); } } } \ No newline at end of file diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 34099949..f9ea77ae 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -425,29 +425,11 @@ export class Map extends L.Map { } } - addTemporaryMarker(latlng: L.LatLng, name: string, coalition: string) { - var marker = new TemporaryUnitMarker(latlng, name, coalition); + addTemporaryMarker(latlng: L.LatLng, name: string, coalition: string, commandHash?: string) { + var marker = new TemporaryUnitMarker(latlng, name, coalition, commandHash); marker.addTo(this); this.#temporaryMarkers.push(marker); - } - - removeTemporaryMarker(latlng: L.LatLng) { - // TODO something more refined than this - var dist: number | null = null; - var closest: L.Marker | null = null; - var i: number = 0; - this.#temporaryMarkers.forEach((marker: L.Marker, idx: number) => { - var t = latlng.distanceTo(marker.getLatLng()); - if (dist == null || t < dist) { - dist = t; - closest = marker; - i = idx; - } - }); - if (closest && dist != null && dist < 100) { - this.removeLayer(closest); - this.#temporaryMarkers.splice(i, 1); - } + return marker; } getSelectedCoalitionArea() { diff --git a/client/src/map/temporaryunitmarker.ts b/client/src/map/temporaryunitmarker.ts index 81136e6a..97f93d4f 100644 --- a/client/src/map/temporaryunitmarker.ts +++ b/client/src/map/temporaryunitmarker.ts @@ -2,15 +2,37 @@ import { CustomMarker } from "./custommarker"; import { DivIcon, LatLng } from "leaflet"; import { SVGInjector } from "@tanem/svg-injector"; import { getMarkerCategoryByName, getUnitDatabaseByCategory } from "../other/utils"; +import { isCommandExecuted } from "../server/server"; +import { getMap } from ".."; export class TemporaryUnitMarker extends CustomMarker { #name: string; #coalition: string; + #commandHash: string|undefined = undefined; + #timer: number = 0; - constructor(latlng: LatLng, name: string, coalition: string) { + constructor(latlng: LatLng, name: string, coalition: string, commandHash?: string) { super(latlng, {interactive: false}); this.#name = name; this.#coalition = coalition; + this.#commandHash = commandHash; + + if (commandHash !== undefined) + this.setCommandHash(commandHash) + } + + setCommandHash(commandHash: string) { + this.#commandHash = commandHash; + this.#timer = window.setInterval(() => { + if (this.#commandHash !== undefined) { + isCommandExecuted((res: any) => { + if (res.commandExecuted) { + this.removeFrom(getMap()); + window.clearInterval(this.#timer); + } + }, this.#commandHash) + } + }, 1000); } createIcon() { diff --git a/client/src/server/server.ts b/client/src/server/server.ts index c92ef643..743d3764 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -1,19 +1,13 @@ import { LatLng } from 'leaflet'; import { getConnectionStatusPanel, getInfoPopup, getLogPanel, getMissionHandler, getServerStatusPanel, getUnitsManager, getWeaponsManager, setLoginStatus } from '..'; import { GeneralSettings, Radio, TACAN } from '../@types/unit'; -import { NONE, ROEs, emissionsCountermeasures, reactionsToThreat } from '../constants/constants'; +import { AIRBASES_URI, BULLSEYE_URI, COMMANDS_URI, LOGS_URI, MISSION_URI, NONE, ROEs, UNITS_URI, WEAPONS_URI, emissionsCountermeasures, reactionsToThreat } from '../constants/constants'; var connected: boolean = false; var paused: boolean = false; var REST_ADDRESS = "http://localhost:30000/olympus"; var DEMO_ADDRESS = window.location.href + "demo"; -const UNITS_URI = "units"; -const WEAPONS_URI = "weapons"; -const LOGS_URI = "logs"; -const AIRBASES_URI = "airbases"; -const BULLSEYE_URI = "bullseyes"; -const MISSION_URI = "mission"; var username = ""; var password = ""; @@ -38,13 +32,15 @@ export function setCredentials(newUsername: string, newPassword: string) { password = newPassword; } -export function GET(callback: CallableFunction, uri: string, options?: { time?: number }, responseType?: string) { +export function GET(callback: CallableFunction, uri: string, options?: ServerRequestOptions, responseType?: string) { var xmlHttp = new XMLHttpRequest(); /* Assemble the request options string */ var optionsString = ''; if (options?.time != undefined) optionsString = `time=${options.time}`; + if (options?.commandHash != undefined) + optionsString = `commandHash=${options.commandHash}`; /* On the connection */ xmlHttp.open("GET", `${demoEnabled ? DEMO_ADDRESS : REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ''}`, true); @@ -92,8 +88,9 @@ export function POST(request: object, callback: CallableFunction) { xmlHttp.setRequestHeader("Content-Type", "application/json"); if (username && password) xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${username}:${password}`)); - xmlHttp.onreadystatechange = () => { - callback(); + xmlHttp.onload = (res: any) => { + var res = JSON.parse(xmlHttp.responseText); + callback(res); }; xmlHttp.send(JSON.stringify(request)); } @@ -140,185 +137,189 @@ export function getWeapons(callback: CallableFunction, refresh: boolean = false) GET(callback, WEAPONS_URI, { time: refresh ? 0 : lastUpdateTimes[WEAPONS_URI] }, 'arraybuffer'); } -export function addDestination(ID: number, path: any) { +export function isCommandExecuted(callback: CallableFunction, commandHash: string) { + GET(callback, COMMANDS_URI, { commandHash: commandHash}); +} + +export function addDestination(ID: number, path: any, callback: CallableFunction = () => {}) { var command = { "ID": ID, "path": path } var data = { "setPath": command } - POST(data, () => { }); + POST(data, callback); } -export function spawnSmoke(color: string, latlng: LatLng) { +export function spawnSmoke(color: string, latlng: LatLng, callback: CallableFunction = () => {}) { var command = { "color": color, "location": latlng }; var data = { "smoke": command } - POST(data, () => { }); + POST(data, callback); } -export function spawnExplosion(intensity: number, latlng: LatLng) { +export function spawnExplosion(intensity: number, latlng: LatLng, callback: CallableFunction = () => {}) { var command = { "intensity": intensity, "location": latlng }; var data = { "explosion": command } - POST(data, () => { }); + POST(data, callback); } -export function spawnAircrafts(units: any, coalition: string, airbaseName: string, country: string, immediate: boolean, spawnPoints: number) { +export function spawnAircrafts(units: any, coalition: string, airbaseName: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => {}) { var command = { "units": units, "coalition": coalition, "airbaseName": airbaseName, "country": country, "immediate": immediate, "spawnPoints": spawnPoints }; var data = { "spawnAircrafts": command } - POST(data, () => { }); + POST(data, callback); } -export function spawnHelicopters(units: any, coalition: string, airbaseName: string, country: string, immediate: boolean, spawnPoints: number) { +export function spawnHelicopters(units: any, coalition: string, airbaseName: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => {}) { var command = { "units": units, "coalition": coalition, "airbaseName": airbaseName, "country": country, "immediate": immediate, "spawnPoints": spawnPoints }; var data = { "spawnHelicopters": command } - POST(data, () => { }); + POST(data, callback); } -export function spawnGroundUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number) { +export function spawnGroundUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => {}) { var command = { "units": units, "coalition": coalition, "country": country, "immediate": immediate, "spawnPoints": spawnPoints };; var data = { "spawnGroundUnits": command } - POST(data, () => { }); + POST(data, callback); } -export function spawnNavyUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number) { +export function spawnNavyUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => {}) { var command = { "units": units, "coalition": coalition, "country": country, "immediate": immediate, "spawnPoints": spawnPoints }; var data = { "spawnNavyUnits": command } - POST(data, () => { }); + POST(data, callback); } -export function attackUnit(ID: number, targetID: number) { +export function attackUnit(ID: number, targetID: number, callback: CallableFunction = () => {}) { var command = { "ID": ID, "targetID": targetID }; var data = { "attackUnit": command } - POST(data, () => { }); + POST(data, callback); } -export function followUnit(ID: number, targetID: number, offset: { "x": number, "y": number, "z": number }) { +export function followUnit(ID: number, targetID: number, offset: { "x": number, "y": number, "z": number }, callback: CallableFunction = () => {}) { // X: front-rear, positive front // Y: top-bottom, positive bottom // Z: left-right, positive right var command = { "ID": ID, "targetID": targetID, "offsetX": offset["x"], "offsetY": offset["y"], "offsetZ": offset["z"] }; var data = { "followUnit": command } - POST(data, () => { }); + POST(data, callback); } -export function cloneUnit(ID: number, latlng: LatLng) { - var command = { "ID": ID, "location": latlng }; - var data = { "cloneUnit": command } - POST(data, () => { }); +export function cloneUnits(units: {ID: number, location: LatLng}[], deleteOriginal: boolean, callback: CallableFunction = () => {}) { + var command = { "units": units, "deleteOriginal": deleteOriginal }; + var data = { "cloneUnits": command } + POST(data, callback); } -export function deleteUnit(ID: number, explosion: boolean, immediate: boolean) { +export function deleteUnit(ID: number, explosion: boolean, immediate: boolean, callback: CallableFunction = () => {}) { var command = { "ID": ID, "explosion": explosion, "immediate": immediate }; var data = { "deleteUnit": command } - POST(data, () => { }); + POST(data, callback); } -export function landAt(ID: number, latlng: LatLng) { +export function landAt(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) { var command = { "ID": ID, "location": latlng }; var data = { "landAt": command } - POST(data, () => { }); + POST(data, callback); } -export function changeSpeed(ID: number, speedChange: string) { +export function changeSpeed(ID: number, speedChange: string, callback: CallableFunction = () => {}) { var command = { "ID": ID, "change": speedChange } var data = { "changeSpeed": command } - POST(data, () => { }); + POST(data, callback); } -export function setSpeed(ID: number, speed: number) { +export function setSpeed(ID: number, speed: number, callback: CallableFunction = () => {}) { var command = { "ID": ID, "speed": speed } var data = { "setSpeed": command } - POST(data, () => { }); + POST(data, callback); } -export function setSpeedType(ID: number, speedType: string) { +export function setSpeedType(ID: number, speedType: string, callback: CallableFunction = () => {}) { var command = { "ID": ID, "speedType": speedType } var data = { "setSpeedType": command } - POST(data, () => { }); + POST(data, callback); } -export function changeAltitude(ID: number, altitudeChange: string) { +export function changeAltitude(ID: number, altitudeChange: string, callback: CallableFunction = () => {}) { var command = { "ID": ID, "change": altitudeChange } var data = { "changeAltitude": command } - POST(data, () => { }); + POST(data, callback); } -export function setAltitudeType(ID: number, altitudeType: string) { +export function setAltitudeType(ID: number, altitudeType: string, callback: CallableFunction = () => {}) { var command = { "ID": ID, "altitudeType": altitudeType } var data = { "setAltitudeType": command } - POST(data, () => { }); + POST(data, callback); } -export function setAltitude(ID: number, altitude: number) { +export function setAltitude(ID: number, altitude: number, callback: CallableFunction = () => {}) { var command = { "ID": ID, "altitude": altitude } var data = { "setAltitude": command } - POST(data, () => { }); + POST(data, callback); } -export function createFormation(ID: number, isLeader: boolean, wingmenIDs: number[]) { +export function createFormation(ID: number, isLeader: boolean, wingmenIDs: number[], callback: CallableFunction = () => {}) { var command = { "ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader } var data = { "setLeader": command } - POST(data, () => { }); + POST(data, callback); } -export function setROE(ID: number, ROE: string) { +export function setROE(ID: number, ROE: string, callback: CallableFunction = () => {}) { var command = { "ID": ID, "ROE": ROEs.indexOf(ROE) } var data = { "setROE": command } - POST(data, () => { }); + POST(data, callback); } -export function setReactionToThreat(ID: number, reactionToThreat: string) { +export function setReactionToThreat(ID: number, reactionToThreat: string, callback: CallableFunction = () => {}) { var command = { "ID": ID, "reactionToThreat": reactionsToThreat.indexOf(reactionToThreat) } var data = { "setReactionToThreat": command } - POST(data, () => { }); + POST(data, callback); } -export function setEmissionsCountermeasures(ID: number, emissionCountermeasure: string) { +export function setEmissionsCountermeasures(ID: number, emissionCountermeasure: string, callback: CallableFunction = () => {}) { var command = { "ID": ID, "emissionsCountermeasures": emissionsCountermeasures.indexOf(emissionCountermeasure) } var data = { "setEmissionsCountermeasures": command } - POST(data, () => { }); + POST(data, callback); } -export function setOnOff(ID: number, onOff: boolean) { +export function setOnOff(ID: number, onOff: boolean, callback: CallableFunction = () => {}) { var command = { "ID": ID, "onOff": onOff } var data = { "setOnOff": command } - POST(data, () => { }); + POST(data, callback); } -export function setFollowRoads(ID: number, followRoads: boolean) { +export function setFollowRoads(ID: number, followRoads: boolean, callback: CallableFunction = () => {}) { var command = { "ID": ID, "followRoads": followRoads } var data = { "setFollowRoads": command } - POST(data, () => { }); + POST(data, callback); } -export function refuel(ID: number) { +export function refuel(ID: number, callback: CallableFunction = () => {}) { var command = { "ID": ID }; var data = { "refuel": command } - POST(data, () => { }); + POST(data, callback); } -export function bombPoint(ID: number, latlng: LatLng) { +export function bombPoint(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) { var command = { "ID": ID, "location": latlng } var data = { "bombPoint": command } - POST(data, () => { }); + POST(data, callback); } -export function carpetBomb(ID: number, latlng: LatLng) { +export function carpetBomb(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) { var command = { "ID": ID, "location": latlng } var data = { "carpetBomb": command } - POST(data, () => { }); + POST(data, callback); } -export function bombBuilding(ID: number, latlng: LatLng) { +export function bombBuilding(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) { var command = { "ID": ID, "location": latlng } var data = { "bombBuilding": command } - POST(data, () => { }); + POST(data, callback); } -export function fireAtArea(ID: number, latlng: LatLng) { +export function fireAtArea(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) { var command = { "ID": ID, "location": latlng } var data = { "fireAtArea": command } - POST(data, () => { }); + POST(data, callback); } -export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings) { +export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings, callback: CallableFunction = () => {}) { var command = { "ID": ID, "isTanker": isTanker, @@ -329,10 +330,10 @@ export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolea }; var data = { "setAdvancedOptions": command }; - POST(data, () => { }); + POST(data, callback); } -export function setCommandModeOptions(restrictSpawns: boolean, restrictToCoalition: boolean, spawnPoints: {blue: number, red: number}, eras: string[], setupTime: number) { +export function setCommandModeOptions(restrictSpawns: boolean, restrictToCoalition: boolean, spawnPoints: {blue: number, red: number}, eras: string[], setupTime: number, callback: CallableFunction = () => {}) { var command = { "restrictSpawns": restrictSpawns, "restrictToCoalition": restrictToCoalition, @@ -342,7 +343,7 @@ export function setCommandModeOptions(restrictSpawns: boolean, restrictToCoaliti }; var data = { "setCommandModeOptions": command }; - POST(data, () => { }); + POST(data, callback); } export function startUpdate() { diff --git a/client/src/unit/unit.ts b/client/src/unit/unit.ts index c3f8c100..a51b0394 100644 --- a/client/src/unit/unit.ts +++ b/client/src/unit/unit.ts @@ -715,8 +715,6 @@ export class Unit extends CustomMarker { /***********************************************/ onAdd(map: Map): this { super.onAdd(map); - /* If this is the first time adding this unit to the map, remove the temporary marker */ - getMap().removeTemporaryMarker(new LatLng(this.#position.lat, this.#position.lng)); return this; } diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index a07cbc5f..5a06ecaf 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -1,7 +1,7 @@ import { LatLng, LatLngBounds } from "leaflet"; import { getHotgroupPanel, getInfoPopup, getMap, getMissionHandler, getUnitsManager, getWeaponsManager } from ".."; import { Unit } from "./unit"; -import { cloneUnit, deleteUnit, refreshAll, spawnAircrafts, spawnGroundUnits, spawnHelicopters, spawnNavyUnits } from "../server/server"; +import { cloneUnits, deleteUnit, spawnAircrafts, spawnGroundUnits, spawnHelicopters, spawnNavyUnits } from "../server/server"; import { bearingAndDistanceToLatLng, deg2rad, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polyContains, polygonArea, randomPointInPoly, randomUnitBlueprint } from "../other/utils"; import { CoalitionArea } from "../map/coalitionarea"; import { groundUnitDatabase } from "./groundunitdatabase"; @@ -12,6 +12,7 @@ import { citiesDatabase } from "./citiesDatabase"; import { aircraftDatabase } from "./aircraftdatabase"; import { helicopterDatabase } from "./helicopterdatabase"; import { navyUnitDatabase } from "./navyunitdatabase"; +import { TemporaryUnitMarker } from "../map/temporaryunitmarker"; export class UnitsManager { #units: { [ID: number]: Unit }; @@ -34,10 +35,10 @@ export class UnitsManager { document.addEventListener('keyup', (event) => this.#onKeyUp(event)); document.addEventListener('exportToFile', () => this.exportToFile()); document.addEventListener('importFromFile', () => this.importFromFile()); - document.addEventListener('contactsUpdated', (e: CustomEvent) => {this.#requestDetectionUpdate = true}); - document.addEventListener('commandModeOptionsChanged', () => {Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility())}); - document.addEventListener('selectedUnitsChangeSpeed', (e: any) => {this.selectedUnitsChangeSpeed(e.detail.type)}); - document.addEventListener('selectedUnitsChangeAltitude', (e: any) => {this.selectedUnitsChangeAltitude(e.detail.type)}); + document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true }); + document.addEventListener('commandModeOptionsChanged', () => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()) }); + document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.selectedUnitsChangeSpeed(e.detail.type) }); + document.addEventListener('selectedUnitsChangeAltitude', (e: any) => { this.selectedUnitsChangeAltitude(e.detail.type) }); } getSelectableAircraft() { @@ -66,7 +67,7 @@ export class UnitsManager { } addUnit(ID: number, category: string) { - if (category){ + if (category) { /* The name of the unit category is exactly the same as the constructor name */ var constructor = Unit.getConstructor(category); if (constructor != undefined) { @@ -91,21 +92,21 @@ export class UnitsManager { return updateTime; } } - this.#units[ID]?.setData(dataExtractor); + this.#units[ID]?.setData(dataExtractor); } if (this.#requestDetectionUpdate && getMissionHandler().getCommandModeOptions().commandMode != GAME_MASTER) { /* Create a dictionary of empty detection methods arrays */ - var detectionMethods: {[key: string]: number[]} = {}; - for (let ID in this.#units) + var detectionMethods: { [key: string]: number[] } = {}; + for (let ID in this.#units) detectionMethods[ID] = []; - for (let ID in getWeaponsManager().getWeapons()) + for (let ID in getWeaponsManager().getWeapons()) detectionMethods[ID] = []; - + /* Fill the array with the detection methods */ for (let ID in this.#units) { const unit = this.#units[ID]; - if (unit.getAlive() && unit.belongsToCommandedCoalition()){ + if (unit.getAlive() && unit.belongsToCommandedCoalition()) { const contacts = unit.getContacts(); contacts.forEach((contact: Contact) => { const contactID = contact.ID; @@ -196,11 +197,11 @@ export class UnitsManager { } } - deselectUnit( ID:number ) { - if ( this.#units.hasOwnProperty( ID ) ) { + deselectUnit(ID: number) { + if (this.#units.hasOwnProperty(ID)) { this.#units[ID].setSelected(false); } else { - console.error( `deselectUnit(): no unit found with ID "${ID}".` ); + console.error(`deselectUnit(): no unit found with ID "${ID}".`); } } @@ -209,38 +210,33 @@ export class UnitsManager { this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setSelected(true)) } - getSelectedUnitsTypes() { - const selectedUnits = this.getSelectedUnits(); - if (selectedUnits.length == 0) + getUnitsTypes(units: Unit[]) { + if (units.length == 0) return []; - return selectedUnits.map((unit: Unit) => { + return units.map((unit: Unit) => { return unit.getCategory(); })?.filter((value: any, index: any, array: string[]) => { return array.indexOf(value) === index; }); - }; + } - /* Gets the value of a variable from the selected units. If all the units have the same value, returns the value, else returns undefined */ - getSelectedUnitsVariable(variableGetter: CallableFunction) { - const selectedUnits = this.getSelectedUnits(); - if (selectedUnits.length == 0) + /* Gets the value of a variable from the units. If all the units have the same value, returns the value, else returns undefined */ + getUnitsVariable(variableGetter: CallableFunction, units: Unit[]) { + if (units.length == 0) return undefined; - return selectedUnits.map((unit: Unit) => { + return units.map((unit: Unit) => { return variableGetter(unit); })?.reduce((a: any, b: any) => { return a === b ? a : undefined }); }; - getSelectedUnitsCoalition() { - const selectedUnits = this.getSelectedUnits(); - if (selectedUnits.length == 0) - return undefined; - return selectedUnits.map((unit: Unit) => { - return unit.getCoalition() - })?.reduce((a: any, b: any) => { - return a == b ? a : undefined - }); + getSelectedUnitsTypes() { + return this.getUnitsTypes(this.getSelectedUnits()); + }; + + getSelectedUnitsVariable(variableGetter: CallableFunction) { + return this.getUnitsVariable(variableGetter, this.getSelectedUnits()); }; getByType(type: string) { @@ -252,10 +248,9 @@ export class UnitsManager { getUnitDetectedMethods(unit: Unit) { var detectionMethods: number[] = []; for (let idx in this.#units) { - if (this.#units[idx].getAlive() && this.#units[idx].getIsLeader() && this.#units[idx].getCoalition() !== "neutral" && this.#units[idx].getCoalition() != unit.getCoalition()) - { + if (this.#units[idx].getAlive() && this.#units[idx].getIsLeader() && this.#units[idx].getCoalition() !== "neutral" && this.#units[idx].getCoalition() != unit.getCoalition()) { this.#units[idx].getContacts().forEach((contact: Contact) => { - if (contact.ID == unit.ID && !detectionMethods.includes(contact.detectionMethod)) + if (contact.ID == unit.ID && !detectionMethods.includes(contact.detectionMethod)) detectionMethods.push(contact.detectionMethod); }); } @@ -414,18 +409,18 @@ export class UnitsManager { selectedUnitsDelete(explosion: boolean = false) { var selectedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ - const selectionContainsAHuman = selectedUnits.some( ( unit:Unit ) => { + const selectionContainsAHuman = selectedUnits.some((unit: Unit) => { return unit.getHuman() === true; }); - if (selectionContainsAHuman && !confirm( "Your selection includes a human player. Deleting humans causes their vehicle to crash.\n\nAre you sure you want to do this?" ) ) { + if (selectionContainsAHuman && !confirm("Your selection includes a human player. Deleting humans causes their vehicle to crash.\n\nAre you sure you want to do this?")) { return; } var immediate = false; if (selectedUnits.length > 20) immediate = confirm(`You are trying to delete ${selectedUnits.length} units, do you want to delete them immediately? This may cause lag for players.`) - + for (let idx in selectedUnits) { selectedUnits[idx].delete(explosion, immediate); } @@ -560,30 +555,28 @@ export class UnitsManager { this.#showActionMessage(selectedUnits, `unit bombing point`); } - // TODO handle from lua selectedUnitsCreateGroup() { var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: false }); - var units = []; - var coalition = "neutral"; + var units: { ID: number, location: LatLng }[] = []; for (let idx in selectedUnits) { var unit = selectedUnits[idx]; - coalition = unit.getCoalition(); - deleteUnit(unit.ID, false, true); - units.push({unitType: unit.getName(), location: unit.getPosition()}); + units.push({ ID: unit.ID, location: unit.getPosition() }); } - const category = this.getSelectedUnitsTypes()[0]; - this.spawnUnits(category, units, coalition, true); + cloneUnits(units, true, () => { + units.forEach((unit: any) => { + deleteUnit(unit.ID, false, false); + }); + }); } /***********************************************/ copyUnits() { - this.#copiedUnits = JSON.parse(JSON.stringify(this.getSelectedUnits().map((unit: Unit) => {return unit.getData()}))); /* Can be applied to humans too */ + this.#copiedUnits = JSON.parse(JSON.stringify(this.getSelectedUnits().map((unit: Unit) => { return unit.getData() }))); /* Can be applied to humans too */ getInfoPopup().setText(`${this.#copiedUnits.length} units copied`); } - // TODO handle from lua pasteUnits() { - if (!this.#pasteDisabled && getMissionHandler().getCommandModeOptions().commandMode == GAME_MASTER) { + if (this.#copiedUnits.length > 0 && !this.#pasteDisabled && getMissionHandler().getCommandModeOptions().commandMode == GAME_MASTER) { /* Compute the position of the center of the copied units */ var nUnits = this.#copiedUnits.length; var avgLat = 0; @@ -595,44 +588,44 @@ export class UnitsManager { } /* Organize the copied units in groups */ - var groups: {[key: string]: any} = {}; + var groups: { [key: string]: any } = {}; this.#copiedUnits.forEach((unit: any) => { if (!(unit.groupName in groups)) groups[unit.groupName] = []; groups[unit.groupName].push(unit); }); + /* Clone the units in groups */ for (let groupName in groups) { - /* Paste the units as groups. Only for ground and navy units because of loadouts, TODO: find a better solution so it works for them too*/ - if (!["Aircraft", "Helicopter"].includes(groups[groupName][0].category)) { - var units = groups[groupName].map((unit: any) => { - var position = new LatLng(getMap().getMouseCoordinates().lat + unit.position.lat - avgLat, getMap().getMouseCoordinates().lng + unit.position.lng - avgLng); - getMap().addTemporaryMarker(position, unit.name, unit.coalition); - return {unitType: unit.name, location: position, liveryID: ""}; - }); - this.spawnUnits(groups[groupName][0].category, units, groups[groupName][0].coalition, true); - } - else { - groups[groupName].forEach((unit: any) => { - var position = new LatLng(getMap().getMouseCoordinates().lat + unit.position.lat - avgLat, getMap().getMouseCoordinates().lng + unit.position.lng - avgLng); - getMap().addTemporaryMarker(position, unit.name, unit.coalition); - cloneUnit(unit.ID, position); - }); - } + var units: { ID: number, location: LatLng }[] = []; + let markers: TemporaryUnitMarker[] = []; + groups[groupName].forEach((unit: any) => { + var position = new LatLng(getMap().getMouseCoordinates().lat + unit.position.lat - avgLat, getMap().getMouseCoordinates().lng + unit.position.lng - avgLng); + markers.push(getMap().addTemporaryMarker(position, unit.name, unit.coalition)); + units.push({ ID: unit.ID, location: position }); + }); + + cloneUnits(units, false, (res: any) => { + if (res.commandHash !== undefined) { + markers.forEach((marker: TemporaryUnitMarker) => { + marker.setCommandHash(res.commandHash); + }) + } + }); } - getInfoPopup().setText(`${this.#copiedUnits.length - 1} units pasted`); + getInfoPopup().setText(`${this.#copiedUnits.length} units pasted`); } else { getInfoPopup().setText(`Unit cloning is disabled in ${getMissionHandler().getCommandModeOptions().commandMode} mode`); } } - createIADS(coalitionArea: CoalitionArea, types: {[key: string]: boolean}, eras: {[key: string]: boolean}, ranges: {[key: string]: boolean}, density: number, distribution: number) { - const activeTypes = Object.keys(types).filter((key: string) => { return types[key]; }); - const activeEras = Object.keys(eras).filter((key: string) => { return eras[key]; }); - const activeRanges = Object.keys(ranges).filter((key: string) => { return ranges[key]; }); + createIADS(coalitionArea: CoalitionArea, types: { [key: string]: boolean }, eras: { [key: string]: boolean }, ranges: { [key: string]: boolean }, density: number, distribution: number) { + const activeTypes = Object.keys(types).filter((key: string) => { return types[key]; }); + const activeEras = Object.keys(eras).filter((key: string) => { return eras[key]; }); + const activeRanges = Object.keys(ranges).filter((key: string) => { return ranges[key]; }); - citiesDatabase.forEach((city: {lat: number, lng: number, pop: number}) => { + citiesDatabase.forEach((city: { lat: number, lng: number, pop: number }) => { if (polyContains(new LatLng(city.lat, city.lng), coalitionArea)) { var pointsNumber = 2 + Math.pow(city.pop, 0.2) * density / 100; for (let i = 0; i < pointsNumber; i++) { @@ -642,10 +635,9 @@ export class UnitsManager { if (polyContains(latlng, coalitionArea)) { const type = activeTypes[Math.floor(Math.random() * activeTypes.length)]; if (Math.random() < IADSDensities[type]) { - const unitBlueprint = randomUnitBlueprint(groundUnitDatabase, {type: type, eras: activeEras, ranges: activeRanges}); + const unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { type: type, eras: activeEras, ranges: activeRanges }); if (unitBlueprint) { - this.spawnUnits("GroundUnit", [{unitType: unitBlueprint.name, location: latlng, liveryID: ""}], coalitionArea.getCoalition(), true); - getMap().addTemporaryMarker(latlng, unitBlueprint.name, coalitionArea.getCoalition()); + this.spawnUnits("GroundUnit", [{ unitType: unitBlueprint.name, location: latlng, liveryID: "" }], coalitionArea.getCoalition(), true); } } } @@ -655,19 +647,19 @@ export class UnitsManager { } exportToFile() { - var unitsToExport: {[key: string]: any} = {}; + var unitsToExport: { [key: string]: any } = {}; for (let ID in this.#units) { var unit = this.#units[ID]; if (!["Aircraft", "Helicopter"].includes(unit.getCategory())) { var data: any = unit.getData(); if (unit.getGroupName() in unitsToExport) unitsToExport[unit.getGroupName()].push(data); - else + else unitsToExport[unit.getGroupName()] = [data]; } } var a = document.createElement("a"); - var file = new Blob([JSON.stringify(unitsToExport)], {type: 'text/plain'}); + var file = new Blob([JSON.stringify(unitsToExport)], { type: 'text/plain' }); a.href = URL.createObjectURL(file); a.download = 'export.json'; a.click(); @@ -682,12 +674,12 @@ export class UnitsManager { return; } var reader = new FileReader(); - reader.onload = function(e: any) { + reader.onload = function (e: any) { var contents = e.target.result; var groups = JSON.parse(contents); for (let groupName in groups) { - if (groupName !== "" && groups[groupName].length > 0 && (groups[groupName].every((unit: any) => {return unit.category == "GroundUnit";}) || groups[groupName].every((unit: any) => {return unit.category == "NavyUnit";}))) { - var aliveUnits = groups[groupName].filter((unit: any) => {return unit.alive}); + if (groupName !== "" && groups[groupName].length > 0 && (groups[groupName].every((unit: any) => { return unit.category == "GroundUnit"; }) || groups[groupName].every((unit: any) => { return unit.category == "NavyUnit"; }))) { + var aliveUnits = groups[groupName].filter((unit: any) => { return unit.alive }); var units = aliveUnits.map((unit: any) => { return { unitType: unit.name, location: unit.position, liveryID: "" } }); @@ -700,40 +692,46 @@ export class UnitsManager { input.click(); } - spawnUnits(category: string, units: any, coalition: string = "blue", immediate: boolean = true, airbase: string = "", country: string = "") { + spawnUnits(category: string, units: any, coalition: string = "blue", immediate: boolean = true, airbase: string = "", country: string = "", callback: CallableFunction = () => {}) { var spawnPoints = 0; + var spawnFunction = () => {}; + var spawnsRestricted = getMissionHandler().getCommandModeOptions().restrictSpawns && getMissionHandler().getRemainingSetupTime() < 0 && getMissionHandler().getCommandModeOptions().commandMode !== GAME_MASTER; + if (category === "Aircraft") { - if (airbase == "" && getMissionHandler().getCommandModeOptions().restrictSpawns && getMissionHandler().getRemainingSetupTime() < 0 && getMissionHandler().getCommandModeOptions().commandMode !== GAME_MASTER) { + if (airbase == "" && spawnsRestricted) { getInfoPopup().setText("Aircrafts can be air spawned during the SETUP phase only"); return false; } spawnPoints = units.reduce((points: number, unit: any) => {return points + aircraftDatabase.getSpawnPointsByName(unit.unitType)}, 0); - spawnAircrafts(units, coalition, airbase, country, immediate, spawnPoints); + spawnFunction = () => spawnAircrafts(units, coalition, airbase, country, immediate, spawnPoints, callback); } else if (category === "Helicopter") { - if (airbase == "" && getMissionHandler().getCommandModeOptions().restrictSpawns && getMissionHandler().getRemainingSetupTime() < 0 && getMissionHandler().getCommandModeOptions().commandMode !== GAME_MASTER) { + if (airbase == "" && spawnsRestricted) { getInfoPopup().setText("Helicopters can be air spawned during the SETUP phase only"); return false; } spawnPoints = units.reduce((points: number, unit: any) => {return points + helicopterDatabase.getSpawnPointsByName(unit.unitType)}, 0); - spawnHelicopters(units, coalition, airbase, country, immediate, spawnPoints); + spawnFunction = () => spawnHelicopters(units, coalition, airbase, country, immediate, spawnPoints, callback); + } else if (category === "GroundUnit") { - if (getMissionHandler().getCommandModeOptions().restrictSpawns && getMissionHandler().getRemainingSetupTime() < 0 && getMissionHandler().getCommandModeOptions().commandMode !== GAME_MASTER) { + if (spawnsRestricted) { getInfoPopup().setText("Ground units can be spawned during the SETUP phase only"); return false; } spawnPoints = units.reduce((points: number, unit: any) => {return points + groundUnitDatabase.getSpawnPointsByName(unit.unitType)}, 0); - spawnGroundUnits(units, coalition, country, immediate, spawnPoints); + spawnFunction = () => spawnGroundUnits(units, coalition, country, immediate, spawnPoints, callback); + } else if (category === "NavyUnit") { - if (getMissionHandler().getCommandModeOptions().restrictSpawns && getMissionHandler().getRemainingSetupTime() < 0 && getMissionHandler().getCommandModeOptions().commandMode !== GAME_MASTER) { + if (spawnsRestricted) { getInfoPopup().setText("Navy units can be spawned during the SETUP phase only"); return false; } spawnPoints = units.reduce((points: number, unit: any) => {return points + navyUnitDatabase.getSpawnPointsByName(unit.unitType)}, 0); - spawnNavyUnits(units, coalition, country, immediate, spawnPoints); + spawnFunction = () => spawnNavyUnits(units, coalition, country, immediate, spawnPoints, callback); } - + if (spawnPoints <= getMissionHandler().getAvailableSpawnPoints()) { getMissionHandler().setSpentSpawnPoints(spawnPoints); + spawnFunction(); return true; } else { getInfoPopup().setText("Not enough spawn points available!"); @@ -747,7 +745,7 @@ export class UnitsManager { if (event.key === "Delete") this.selectedUnitsDelete(); else if (event.key === "a" && event.ctrlKey) - Object.values(this.getUnits()).filter((unit: Unit) => {return !unit.getHidden()}).forEach((unit: Unit) => unit.setSelected(true)); + Object.values(this.getUnits()).filter((unit: Unit) => { return !unit.getHidden() }).forEach((unit: Unit) => unit.setSelected(true)); } } diff --git a/client/src/weapon/weapon.ts b/client/src/weapon/weapon.ts index 911794f5..1ee6b946 100644 --- a/client/src/weapon/weapon.ts +++ b/client/src/weapon/weapon.ts @@ -214,8 +214,6 @@ export class Weapon extends CustomMarker { /***********************************************/ onAdd(map: Map): this { super.onAdd(map); - /* If this is the first time adding this unit to the map, remove the temporary marker */ - getMap().removeTemporaryMarker(new LatLng(this.#position.lat, this.#position.lng)); return this; } diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 29d285bf..32f3fa70 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -1,39 +1,50 @@ local version = "v0.4.4-alpha" -local debug = false +local debug = true -- True enables debug printing using DCS messages +-- .dll related variables Olympus.OlympusDLL = nil Olympus.DLLsloaded = false Olympus.OlympusModPath = os.getenv('DCSOLYMPUS_PATH')..'\\bin\\' + +-- Logger reference Olympus.log = mist.Logger:new("Olympus", 'info') -Olympus.unitCounter = 1 -Olympus.payloadRegistry = {} - +-- Data structures for transfer to .dll Olympus.missionData = {} Olympus.unitsData = {} Olympus.weaponsData = {} -Olympus.unitIndex = 0 -Olympus.unitStep = 50 -Olympus.units = {} +-- Units data structures +Olympus.unitCounter = 1 -- Counter to generate unique names +Olympus.spawnDatabase = {} -- Database of spawn options, used for units cloning +Olympus.unitIndex = 0 -- Counter used to spread the computational load of data retrievial from DCS +Olympus.unitStep = 50 -- Max number of units that get updated each cycle +Olympus.units = {} -- Table holding references to all the currently existing units -Olympus.weaponIndex = 0 -Olympus.weaponStep = 50 -Olympus.weapons = {} +Olympus.weaponIndex = 0 -- Counter used to spread the computational load of data retrievial from DCS +Olympus.weaponStep = 50 -- Max number of weapons that get updated each cycle +Olympus.weapons = {} -- Table holding references to all the currently existing weapons +-- Miscellaneous initializations Olympus.missionStartTime = DCS.getRealTime() +------------------------------------------------------------------------------------------------------ +-- Olympus functions +------------------------------------------------------------------------------------------------------ +-- Print a debug message if the debug option is true function Olympus.debug(message, displayFor) if debug == true then trigger.action.outText(message, displayFor) end end +-- Print a notify message function Olympus.notify(message, displayFor) trigger.action.outText(message, displayFor) end +-- Loads the olympus .dll function Olympus.loadDLLs() -- Add the .dll paths package.cpath = package.cpath..';'..Olympus.OlympusModPath..'?.dll;' @@ -52,6 +63,7 @@ function Olympus.getUnitByID(ID) return Olympus.units[ID]; end +-- Gets the ID of the first country that belongs to a coalition function Olympus.getCountryIDByCoalition(coalitionString) for countryName, countryId in pairs(country.id) do if coalition.getCountryCoalition(countryId) == Olympus.getCoalitionIDByCoalition(coalitionString) then @@ -61,6 +73,7 @@ function Olympus.getCountryIDByCoalition(coalitionString) return 0 end +-- Gets the coalition ID of a coalition function Olympus.getCoalitionIDByCoalition(coalitionString) local coalitionID = 0 if coalitionString == "red" then @@ -71,6 +84,7 @@ function Olympus.getCoalitionIDByCoalition(coalitionString) return coalitionID end +-- Gets the coalition name from the coalition ID function Olympus.getCoalitionByCoalitionID(coalitionID) local coalitionString = "neutral" if coalitionID == 1 then @@ -81,7 +95,7 @@ function Olympus.getCoalitionByCoalitionID(coalitionID) return coalitionString end --- Builds a valid task depending on the provided options +-- Builds a valid enroute task depending on the provided options function Olympus.buildEnrouteTask(options) local task = nil -- Engage specific target by ID. Checks if target exists. @@ -111,11 +125,13 @@ function Olympus.buildEnrouteTask(options) return task end --- Builds a valid task depending on the provided options +-- Builds a valid main task depending on the provided options function Olympus.buildTask(groupName, options) local task = nil local group = Group.getByName(groupName) + + -- Combo tasks require nested tables if (Olympus.isArray(options)) then local tasks = {} for idx, subOptions in pairs(options) do @@ -129,6 +145,7 @@ function Olympus.buildTask(groupName, options) } Olympus.debug(Olympus.serializeTable(task), 30) else + -- Follow a unit in formation with a given offset if options['id'] == 'FollowUnit' and options['leaderID'] and options['offset'] then local leader = Olympus.getUnitByID(options['leaderID']) if leader and leader:isExist() then @@ -142,11 +159,13 @@ function Olympus.buildTask(groupName, options) } } end + -- Go refuel to the nearest tanker. If the unit can't refuel it will RTB elseif options['id'] == 'Refuel' then task = { id = 'Refueling', params = {} } + -- Orbit in place at a given altitude and with a given pattern elseif options['id'] == 'Orbit' then task = { id = 'Orbit', @@ -173,6 +192,7 @@ function Olympus.buildTask(groupName, options) if options['speed'] then task['params']['speed'] = options['speed'] end + -- Bomb a specific location elseif options['id'] == 'Bombing' and options['lat'] and options['lng'] then local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { @@ -182,6 +202,7 @@ function Olympus.buildTask(groupName, options) attackQty = 1 } } + -- Perform carpet bombing at a specific location elseif options['id'] == 'CarpetBombing' and options['lat'] and options['lng'] then local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { @@ -196,14 +217,7 @@ function Olympus.buildTask(groupName, options) attackQtyLimit = true } } - elseif options['id'] == 'AttackMapObject' and options['lat'] and options['lng'] then - local point = coord.LLtoLO(options['lat'], options['lng'], 0) - task = { - id = 'AttackMapObject', - params = { - point = {x = point.x, y = point.z} - } - } + -- Fire at a specific point elseif options['id'] == 'FireAtPoint' and options['lat'] and options['lng'] and options['radius'] then local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { @@ -226,22 +240,17 @@ function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedT if category == "Aircraft" then local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) + + -- 'AGL' mode does not appear to work in the buildWP function. This is a crude approximation if altitudeType == "AGL" then altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude end - local path = {} - if taskOptions and taskOptions['id'] == 'Land' then - path = { - [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), - [2] = mist.fixedWing.buildWP(endPoint, landing, speed, 0, 'AGL') - } - else - path = { - [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), - [2] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') - } - end + -- Create the path + local path = { + [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), + [2] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') + } -- If a task exists assign it to the controller if taskOptions then @@ -270,22 +279,16 @@ function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedT local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) + -- 'AGL' mode does not appear to work in the buildWP function. This is a crude approximation if altitudeType == "AGL" then altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude end - local path = {} - if taskOptions and taskOptions['id'] == 'Land' then - path = { - [1] = mist.heli.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), - [2] = mist.heli.buildWP(endPoint, landing, speed, 0, 'AGL') - } - else - path = { - [1] = mist.heli.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), - [2] = mist.heli.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') - } - end + -- Create the path + local path = { + [1] = mist.heli.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), + [2] = mist.heli.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') + } -- If a task exists assign it to the controller if taskOptions then @@ -315,13 +318,12 @@ function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedT local endPoint = coord.LLtoLO(lat, lng, 0) local bearing = math.atan2(endPoint.z - startPoint.z, endPoint.x - startPoint.x) - vars = - { - group = group, - point = endPoint, - heading = bearing, - speed = speed - } + vars = { + group = group, + point = endPoint, + heading = bearing, + speed = speed + } if taskOptions and taskOptions['id'] == 'FollowRoads' and taskOptions['value'] == true then vars["disableRoads"] = false @@ -331,21 +333,20 @@ function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedT end mist.groupToRandomPoint(vars) - Olympus.debug("Olympus.move executed succesfully on GroundUnit", 2) + Olympus.debug("Olympus.move executed successfully on GroundUnit", 2) elseif category == "NavyUnit" then local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) local bearing = math.atan2(endPoint.z - startPoint.z, endPoint.x - startPoint.x) - vars = - { - group = group, - point = endPoint, - heading = bearing, - speed = speed - } + vars = { + group = group, + point = endPoint, + heading = bearing, + speed = speed + } mist.groupToRandomPoint(vars) - Olympus.debug("Olympus.move executed succesfully on NavyUnit", 2) + Olympus.debug("Olympus.move executed successfully on NavyUnit", 2) else Olympus.debug("Olympus.move not implemented yet for " .. category, 2) end @@ -379,31 +380,44 @@ function Olympus.explosion(intensity, lat, lng) end -- Spawns a new unit or group +-- Spawn table contains the following parameters +-- category: (string), either Aircraft, Helicopter, GroundUnit or NavyUnit +-- coalition: (string) +-- country: (string) +-- airbaseName: (string, optional) only for air units +-- units: (array) Array of units to spawn. All units will be in the same group. Each unit element must contain: + -- unitType: (string) DCS Name of the unit + -- lat: (number) + -- lng: (number) + -- alt: (number, optional) only for air units + -- loadout: (string, optional) only for air units, must be one of the loadouts defined in unitPayloads.lua + -- payload: (table, optional) overrides loadout, specifies directly the loadout of the unit + -- liveryID: (string, optional) function Olympus.spawnUnits(spawnTable) Olympus.debug("Olympus.spawnUnits " .. Olympus.serializeTable(spawnTable), 2) - local unitTable = nil + local unitsTable = nil local route = nil local category = nil + -- Generate the units table and rout as per DCS requirements if spawnTable.category == 'Aircraft' then - unitTable = Olympus.generateAirUnitsTable(spawnTable.units) + unitsTable = Olympus.generateAirUnitsTable(spawnTable.units) route = Olympus.generateAirUnitsRoute(spawnTable) category = 'plane' elseif spawnTable.category == 'Helicopter' then - unitTable = Olympus.generateAirUnitsTable(spawnTable.units) + unitsTable = Olympus.generateAirUnitsTable(spawnTable.units) route = Olympus.generateAirUnitsRoute(spawnTable) category = 'helicopter' elseif spawnTable.category == 'GroundUnit' then - unitTable = Olympus.generateGroundUnitsTable(spawnTable.units) + unitsTable = Olympus.generateGroundUnitsTable(spawnTable.units) category = 'vehicle' elseif spawnTable.category == 'NavyUnit' then - unitTable = Olympus.generateNavyUnitsTable(spawnTable.units) + unitsTable = Olympus.generateNavyUnitsTable(spawnTable.units) category = 'ship' end - Olympus.debug(Olympus.serializeTable(unitTable), 5) - + -- It the unit country is not specified, get a country that belongs to the coalition local countryID = 0 if spawnTable.country == nil or spawnTable.country == "" then countryID = Olympus.getCountryIDByCoalition(spawnTable.coalition) @@ -411,9 +425,15 @@ function Olympus.spawnUnits(spawnTable) countryID = country.id[spawnTable.country] end + -- Save the units in the database, for cloning + for idx, unitTable in pairs(unitsTable) do + Olympus.addToDatabase(unitTable) + end + + -- Spawn the new group local vars = { - units = unitTable, + units = unitsTable, country = countryID, category = category, route = route, @@ -426,9 +446,9 @@ function Olympus.spawnUnits(spawnTable) Olympus.debug("Olympus.spawnUnits completed succesfully", 2) end --- Generates unit table for a air unit. +-- Generates unit table for air units function Olympus.generateAirUnitsTable(units) - local unitTable = {} + local unitsTable = {} for idx, unit in pairs(units) do local loadout = unit.loadout -- loadout: a string, one of the names defined in unitPayloads.lua. Must be compatible with the unitType local payload = unit.payload -- payload: a table, if present the unit will receive this specific payload. Overrides loadout @@ -437,6 +457,7 @@ function Olympus.generateAirUnitsTable(units) unit.heading = 0 end + -- Define the loadout if payload == nil then if loadout and loadout ~= "" and Olympus.unitPayloads[unit.unitType][loadout] then payload = Olympus.unitPayloads[unit.unitType][loadout] @@ -445,8 +466,9 @@ function Olympus.generateAirUnitsTable(units) end end + -- Generate the unit table local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0)) - unitTable[#unitTable + 1] = + unitsTable[#unitsTable + 1] = { ["type"] = unit.unitType, ["x"] = spawnLocation.x, @@ -456,15 +478,12 @@ function Olympus.generateAirUnitsTable(units) ["skill"] = "Excellent", ["payload"] = { ["pylons"] = payload, ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100, }, ["heading"] = unit.heading, - ["callsign"] = { [1] = 1, [2] = 1, [3] = 1, ["name"] = "Olympus" .. Olympus.unitCounter.. "-" .. #unitTable + 1 }, - ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitTable + 1, + ["callsign"] = { [1] = 1, [2] = 1, [3] = 1, ["name"] = "Olympus" .. Olympus.unitCounter.. "-" .. #unitsTable + 1 }, + ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1, ["livery_id"] = unit.liveryID } - - -- Add the payload to the registry, used for unit cloning - Olympus.payloadRegistry[unitTable[#unitTable].name] = payload end - return unitTable + return unitsTable end function Olympus.generateAirUnitsRoute(spawnTable) @@ -525,102 +544,192 @@ end -- Generates ground units table, either single or from template function Olympus.generateGroundUnitsTable(units) - local unitTable = {} + local unitsTable = {} for idx, unit in pairs(units) do local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0)) if Olympus.hasKey(templates, unit.unitType) then for idx, value in pairs(templates[unit.unitType].units) do - unitTable[#unitTable + 1] = + unitsTable[#unitsTable + 1] = { ["type"] = value.name, ["x"] = spawnLocation.x + value.dx, ["y"] = spawnLocation.z + value.dy, ["heading"] = 0, ["skill"] = "High", - ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitTable + 1 + ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1 } end else - unitTable[#unitTable + 1] = + unitsTable[#unitsTable + 1] = { ["type"] = unit.unitType, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, ["heading"] = unit.heading, ["skill"] = "High", - ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitTable + 1, + ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1, ["livery_id"] = unit.liveryID } end end - return unitTable + return unitsTable end -- Generates navy units table, either single or from template function Olympus.generateNavyUnitsTable(units) - local unitTable = {} + local unitsTable = {} for idx, unit in pairs(units) do local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0)) if Olympus.hasKey(templates, unit.unitType) then for idx, value in pairs(templates[unit.unitType].units) do - unitTable[#unitTable + 1] = + unitsTable[#unitsTable + 1] = { ["type"] = value.name, ["x"] = spawnLocation.x + value.dx, ["y"] = spawnLocation.z + value.dy, ["heading"] = 0, ["skill"] = "High", - ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitTable + 1, + ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1, ["transportable"] = { ["randomTransportable"] = false } } end else - unitTable[#unitTable + 1] = + unitsTable[#unitsTable + 1] = { ["type"] = unit.unitType, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, ["heading"] = unit.heading, ["skill"] = "High", - ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitTable + 1, + ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1, ["transportable"] = { ["randomTransportable"] = false }, ["livery_id"] = unit.liveryID } end end - return unitTable + return unitsTable end --- Clones a unit by ID. Will clone the unit with the same original payload as the source unit. TODO: only works on Olympus unit not ME units. -function Olympus.clone(ID, lat, lng, category) - Olympus.debug("Olympus.clone " .. ID .. ", " .. category, 2) - local unit = Olympus.getUnitByID(ID) - if unit then - local position = unit:getPosition() - local heading = math.atan2( position.x.z, position.x.x ) - - -- TODO: understand category in this script - local spawnTable = { - coalition = Olympus.getCoalitionByCoalitionID(unit:getCoalition()), - category = category, - units = { - [1] = { - lat = lat, - lng = lng, - alt = unit:getPoint().y, - heading = heading, - unitType = unit:getTypeName(), - payload = Olympus.payloadRegistry[unit:getName()] - } - } - } - Olympus.spawnUnits(spawnTable) +-- Add the unit data to the database, used for unit cloning +function Olympus.addToDatabase(unitTable) + Olympus.spawnDatabase[unitTable.name] = unitTable +end + +-- Clones a unit by ID. Will clone the unit with the same original payload as the source unit. TODO: only works on Olympus unit not ME units (TO BE VERIFIED). +-- cloneTable is an array of element, each of which contains + -- ID: (number) ID of the unit to clone + -- lat: (number) + -- lng: (number) +function Olympus.clone(cloneTable, deleteOriginal) + Olympus.debug("Olympus.clone " .. Olympus.serializeTable(cloneTable), 2) + + local unitsTable = {} + local countryID = nil + local category = nil + local route = {} + + -- All the units in the table will be cloned in a single group + for idx, cloneData in pairs(cloneTable) do + local ID = cloneData.ID + local unit = Olympus.getUnitByID(ID) + + if unit then + local position = unit:getPosition() + local heading = math.atan2( position.x.z, position.x.x ) + + -- Update the data of the cloned unit + local unitTable = mist.utils.deepCopy(Olympus.spawnDatabase[unit:getName()]) + + local point = coord.LLtoLO(cloneData['lat'], cloneData['lng'], 0) + if unitTable then + unitTable["x"] = point.x + unitTable["y"] = point.z + unitTable["alt"] = unit:getPoint().y + unitTable["heading"] = heading + unitTable["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitsTable + 1 + end + + if countryID == nil and category == nil then + countryID = unit:getCountry() + if unit:getDesc().category == Unit.Category.AIRPLANE then + category = 'plane' + route = { + ["points"] = + { + [1] = + { + ["alt"] = alt, + ["alt_type"] = "BARO", + ["tasks"] = { + [1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, }, + [2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, }, + }, + ["type"] = "Turning Point", + ["x"] = point.x, + ["y"] = point.z, + }, + }, + } + elseif unit:getDesc().category == Unit.Category.HELICOPTER then + category = 'helicopter' + route = { + ["points"] = + { + [1] = + { + ["alt"] = alt, + ["alt_type"] = "BARO", + ["tasks"] = { + [1] = {["number"] = 1, ["auto"] = true, ["id"] = "WrappedAction", ["enabled"] = true, ["params"] = {["action"] = {["id"] = "EPLRS", ["params"] = {["value"] = true}, }, }, }, + [2] = {["number"] = 2, ["auto"] = false, ["id"] = "Orbit", ["enabled"] = true, ["params"] = {["pattern"] = "Circle"}, }, + }, + ["type"] = "Turning Point", + ["x"] = point.x, + ["y"] = point.z, + }, + }, + } + elseif unit:getDesc().category == Unit.Category.GROUND_UNIT then + category = 'vehicle' + elseif unit:getDesc().category == Unit.Category.SHIP then + category = 'ship' + end + end + + unitsTable[#unitsTable + 1] = mist.utils.deepCopy(unitTable) + end + + if deleteOriginal then + Olympus.delete(ID, false) + end end + + local vars = + { + units = unitsTable, + country = countryID, + category = category, + route = route, + name = "Olympus-" .. Olympus.unitCounter, + task = 'CAP' + } + + Olympus.debug(Olympus.serializeTable(vars), 1) + + -- Save the units in the database, for cloning + for idx, unitTable in pairs(unitsTable) do + Olympus.addToDatabase(unitTable) + end + + mist.dynAdd(vars) + Olympus.unitCounter = Olympus.unitCounter + 1 + Olympus.debug("Olympus.clone completed successfully", 2) end +-- Delete a unit by ID, optionally use an explosion function Olympus.delete(ID, explosion) Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2) local unit = Olympus.getUnitByID(ID) @@ -635,6 +744,7 @@ function Olympus.delete(ID, explosion) end end +-- Set a DCS main task to a group function Olympus.setTask(groupName, taskOptions) Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2) local group = Group.getByName(groupName) @@ -648,6 +758,7 @@ function Olympus.setTask(groupName, taskOptions) end end +-- Reset the dask of a group function Olympus.resetTask(groupName) Olympus.debug("Olympus.resetTask " .. groupName, 2) local group = Group.getByName(groupName) @@ -657,6 +768,7 @@ function Olympus.resetTask(groupName) end end +-- Give a group a command function Olympus.setCommand(groupName, command) Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2) local group = Group.getByName(groupName) @@ -666,6 +778,7 @@ function Olympus.setCommand(groupName, command) end end +-- Set an option of a group function Olympus.setOption(groupName, optionID, optionValue) Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2) local group = Group.getByName(groupName) @@ -675,6 +788,7 @@ function Olympus.setOption(groupName, optionID, optionValue) end end +-- Disable the AI of a group on or off entirely function Olympus.setOnOff(groupName, onOff) Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2) local group = Group.getByName(groupName) @@ -684,6 +798,7 @@ function Olympus.setOnOff(groupName, onOff) end end +-- This function is periodically called to collect the data of all the existing units in the mission to be transmitted to the olympus.dll function Olympus.setUnitsData(arg, time) -- Units data local units = {} @@ -693,9 +808,11 @@ function Olympus.setUnitsData(arg, time) local index = 0 for ID, unit in pairs(Olympus.units) do index = index + 1 + -- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles if index > startIndex then if unit ~= nil then - local table = {} + local table = {} + -- Get the object category in Olympus name local objectCategory = unit:getCategory() if objectCategory == Object.Category.UNIT then @@ -765,6 +882,7 @@ function Olympus.setUnitsData(arg, time) end end else + -- If the unit reference is nil it means the unit no longer exits units[ID] = {isAlive = false} Olympus.units[ID] = nil end @@ -773,6 +891,8 @@ function Olympus.setUnitsData(arg, time) break end end + + -- Reset the counter if index ~= endIndex then Olympus.unitIndex = 0 else @@ -786,6 +906,7 @@ function Olympus.setUnitsData(arg, time) return time + 0.05 end +-- This function is periodically called to collect the data of all the existing weapons in the mission to be transmitted to the olympus.dll function Olympus.setWeaponsData(arg, time) -- Weapons data local weapons = {} @@ -795,6 +916,8 @@ function Olympus.setWeaponsData(arg, time) local index = 0 for ID, weapon in pairs(Olympus.weapons) do index = index + 1 + + -- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles if index > startIndex then if weapon ~= nil then local table = {} @@ -835,6 +958,7 @@ function Olympus.setWeaponsData(arg, time) weapons[ID] = table end else + -- If the weapon reference is nil it means the unit no longer exits weapons[ID] = {isAlive = false} Olympus.weapons[ID] = nil end @@ -843,6 +967,8 @@ function Olympus.setWeaponsData(arg, time) break end end + + -- Reset the counter if index ~= endIndex then Olympus.weaponIndex = 0 else @@ -910,13 +1036,14 @@ function Olympus.setMissionData(arg, time) Olympus.missionData["mission"] = mission Olympus.OlympusDLL.setMissionData() - return time + 1 + return time + 1 -- For perfomance reasons weapons are updated once every second end +-- Initializes the units table with all the existing ME units function Olympus.initializeUnits() if mist and mist.DBs and mist.DBs.MEunitsById then - for id, unitTable in pairs(mist.DBs.MEunitsById) do - local unit = Unit.getByName(unitTable["unitName"]) + for id, unitsTable in pairs(mist.DBs.MEunitsById) do + local unit = Unit.getByName(unitsTable["unitName"]) if unit then Olympus.units[unit["id_"]] = unit end @@ -928,6 +1055,9 @@ function Olympus.initializeUnits() end end +------------------------------------------------------------------------------------------------------ +-- Olympus utility functions +------------------------------------------------------------------------------------------------------ function Olympus.serializeTable(val, name, skipnewlines, depth) skipnewlines = skipnewlines or false depth = depth or 0 diff --git a/src/core/include/commands.h b/src/core/include/commands.h index 376e3443..58279826 100644 --- a/src/core/include/commands.h +++ b/src/core/include/commands.h @@ -98,9 +98,11 @@ public: unsigned int getPriority() { return priority; } virtual string getString() = 0; virtual unsigned int getLoad() = 0; + const string getHash() { return hash; } protected: unsigned int priority = CommandPriority::LOW; + const string hash = random_string(16); }; /* Simple low priority move command (from user click) */ @@ -248,9 +250,9 @@ private: class Clone : public Command { public: - Clone(unsigned int ID, Coords location) : - ID(ID), - location(location) + Clone(vector cloneOptions, bool deleteOriginal) : + cloneOptions(cloneOptions), + deleteOriginal(deleteOriginal) { priority = CommandPriority::LOW; }; @@ -258,8 +260,8 @@ public: virtual unsigned int getLoad() { return 30; } private: - const unsigned int ID; - const Coords location; + const vector cloneOptions; + const bool deleteOriginal; }; /* Delete unit command */ diff --git a/src/core/include/datatypes.h b/src/core/include/datatypes.h index 0e776308..f3859456 100644 --- a/src/core/include/datatypes.h +++ b/src/core/include/datatypes.h @@ -120,4 +120,9 @@ struct SpawnOptions { Coords location; string loadout; string liveryID; +}; + +struct CloneOptions { + unsigned int ID; + Coords location; }; \ No newline at end of file diff --git a/src/core/include/scheduler.h b/src/core/include/scheduler.h index 3cd2fe30..37acdffe 100644 --- a/src/core/include/scheduler.h +++ b/src/core/include/scheduler.h @@ -11,9 +11,10 @@ public: void appendCommand(Command* command); void execute(lua_State* L); - void handleRequest(string key, json::value value, string username); + void handleRequest(string key, json::value value, string username, json::value& answer); bool checkSpawnPoints(int spawnPoints, string coalition); - + bool isCommandExecuted(string commandHash) { return (find(executedCommandsHashes.begin(), executedCommandsHashes.end(), commandHash) != executedCommandsHashes.end()); } + void setFrameRate(double newFrameRate) { frameRate = newFrameRate; } void setRestrictSpawns(bool newRestrictSpawns) { restrictSpawns = newRestrictSpawns; } void setRestrictToCoalition(bool newRestrictToCoalition) { restrictToCoalition = newRestrictToCoalition; } @@ -35,6 +36,7 @@ public: private: list commands; + list executedCommandsHashes; unsigned int load; double frameRate; diff --git a/src/core/src/commands.cpp b/src/core/src/commands.cpp index fbfd3779..4ab06ba9 100644 --- a/src/core/src/commands.cpp +++ b/src/core/src/commands.cpp @@ -140,22 +140,22 @@ string SpawnHelicopters::getString() /* Clone unit command */ string Clone::getString() { - Unit* unit = unitsManager->getUnit(ID); - if (unit != nullptr) - { - std::ostringstream commandSS; - commandSS.precision(10); - commandSS << "Olympus.clone, " - << ID << ", " - << location.lat << ", " - << location.lng << ", " - << "\"" << unit->getCategory() << "\""; - return commandSS.str(); - } - else - { - return ""; + std::ostringstream unitsSS; + unitsSS.precision(10); + for (int i = 0; i < cloneOptions.size(); i++) { + unitsSS << "[" << i + 1 << "] = {" + << "ID = " << cloneOptions[i].ID << ", " + << "lat = " << cloneOptions[i].location.lat << ", " + << "lng = " << cloneOptions[i].location.lng << " }, "; } + + std::ostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.clone, " + << "{" << unitsSS.str() << "}" << ", " + << (deleteOriginal ? "true" : "false"); + return commandSS.str(); + } /* Delete unit command */ diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp index a9c88b78..742f5c5b 100644 --- a/src/core/src/scheduler.cpp +++ b/src/core/src/scheduler.cpp @@ -60,6 +60,7 @@ void Scheduler::execute(lua_State* L) log("Command '" + commandString + "' executed correctly, current load " + to_string(getLoad())); load = command->getLoad(); commands.remove(command); + executedCommandsHashes.push_back(command->getHash()); delete command; return; } @@ -134,7 +135,7 @@ bool Scheduler::checkSpawnPoints(int spawnPoints, string coalition) } } -void Scheduler::handleRequest(string key, json::value value, string username) +void Scheduler::handleRequest(string key, json::value value, string username, json::value& answer) { Command* command = nullptr; @@ -376,13 +377,21 @@ void Scheduler::handleRequest(string key, json::value value, string username) if (unit != nullptr) unit->setDesiredAltitudeType(to_string(value[L"altitudeType"])); } - else if (key.compare("cloneUnit") == 0) + else if (key.compare("cloneUnits") == 0) { - unsigned int ID = value[L"ID"].as_integer(); - double lat = value[L"location"][L"lat"].as_double(); - double lng = value[L"location"][L"lng"].as_double(); - Coords loc; loc.lat = lat; loc.lng = lng; - command = dynamic_cast(new Clone(ID, loc)); + vector cloneOptions; + bool deleteOriginal = value[L"deleteOriginal"].as_bool(); + + for (auto unit : value[L"units"].as_array()) { + unsigned int ID = unit[L"ID"].as_integer(); + double lat = unit[L"location"][L"lat"].as_double(); + double lng = unit[L"location"][L"lng"].as_double(); + + Coords location; location.lat = lat; location.lng = lng; + cloneOptions.push_back({ ID, location }); + } + + command = dynamic_cast(new Clone(cloneOptions, deleteOriginal)); } else if (key.compare("setROE") == 0) { @@ -565,6 +574,7 @@ void Scheduler::handleRequest(string key, json::value value, string username) { appendCommand(command); log("New command appended correctly to stack. Current server load: " + to_string(getLoad())); + answer[L"commandHash"] = json::value(to_wstring(command->getHash())); } } diff --git a/src/core/src/server.cpp b/src/core/src/server.cpp index b707919f..2817846a 100644 --- a/src/core/src/server.cpp +++ b/src/core/src/server.cpp @@ -142,6 +142,9 @@ void Server::handle_get(http_request request) else answer[L"mission"][L"commandModeOptions"][L"commandMode"] = json::value(L"Observer"); } + else if (URI.compare(COMMANDS_URI) == 0 && query.find(L"commandHash") != query.end()) { + answer[L"commandExecuted"] = json::value(scheduler->isCommandExecuted(to_string(query[L"commandHash"]))); + } /* Common data */ answer[L"time"] = json::value::string(to_wstring(ms.count())); @@ -222,7 +225,7 @@ void Server::handle_put(http_request request) std::exception_ptr eptr; try { - scheduler->handleRequest(to_string(key), value, username); + scheduler->handleRequest(to_string(key), value, username, answer); } catch (...) { eptr = std::current_exception(); // capture diff --git a/src/shared/include/defines.h b/src/shared/include/defines.h index 29d51209..e8f8fc14 100644 --- a/src/shared/include/defines.h +++ b/src/shared/include/defines.h @@ -10,5 +10,6 @@ #define AIRBASES_URI "airbases" #define BULLSEYE_URI "bullseyes" #define MISSION_URI "mission" +#define COMMANDS_URI "commands" #define FRAMERATE_TIME_INTERVAL 0.05 \ No newline at end of file