diff --git a/client/src/index.ts b/client/src/index.ts index 4a5c9891..f95e4d5a 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -28,6 +28,7 @@ var mouseInfoPanel: MouseInfoPanel; var logPanel: LogPanel; var connected: boolean = false; +var paused: boolean = false; var activeCoalition: string = "blue"; var sessionHash: string | null = null; @@ -106,8 +107,10 @@ function startPeriodicUpdate() { function requestUpdate() { /* Main update rate = 250ms is minimum time, equal to server update time. */ getUnits((data: UnitsData) => { - getUnitsManager()?.update(data); - checkSessionHash(data.sessionHash); + if (!getPaused()){ + getUnitsManager()?.update(data); + checkSessionHash(data.sessionHash); + } }, false); setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); @@ -117,15 +120,17 @@ function requestUpdate() { function requestRefresh() { /* Main refresh rate = 5000ms. */ getUnits((data: UnitsData) => { - getUnitsManager()?.update(data); - getAirbases((data: AirbasesData) => getMissionData()?.update(data)); - getBullseyes((data: BullseyesData) => getMissionData()?.update(data)); - getMission((data: any) => {getMissionData()?.update(data)}); + if (!getPaused()){ + getUnitsManager()?.update(data); + getAirbases((data: AirbasesData) => getMissionData()?.update(data)); + getBullseyes((data: BullseyesData) => getMissionData()?.update(data)); + getMission((data: any) => {getMissionData()?.update(data)}); - // Update the list of existing units - getUnitDataTable()?.update(); - - checkSessionHash(data.sessionHash); + // Update the list of existing units + getUnitDataTable()?.update(); + + checkSessionHash(data.sessionHash); + } }, true); setTimeout(() => requestRefresh(), 5000); } @@ -183,6 +188,9 @@ function setupEvents() { case "Quote": unitDataTable.toggle(); break + case "Space": + setPaused(!getPaused()); + break; } }); @@ -268,4 +276,12 @@ export function getConnected() { return connected; } +export function setPaused(newPaused: boolean) { + paused = newPaused; +} + +export function getPaused() { + return paused; +} + window.onload = setup; \ No newline at end of file diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index e520eb45..1880eba9 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -210,7 +210,6 @@ export class UnitControlPanel extends Panel { if (getUnitsManager().getSelectedUnits().length == 1) { - var asd = this.#advancedSettingsDialog; this.#radioCallsignDropdown.setOptions(["Enfield", "Springfield", "Uzi", "Colt", "Dodge", "Ford", "Chevy", "Pontiac"]); this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign); var tankerCheckbox = this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.querySelector("input") diff --git a/client/src/server/server.ts b/client/src/server/server.ts index 1bbe50cf..7a97967b 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -118,7 +118,11 @@ export function attackUnit(ID: number, targetID: number) { } export function followUnit(ID: number, targetID: number) { - var command = { "ID": ID, "targetID": targetID }; + // X: front-rear, positive front + // Y: top-bottom, positive bottom + // Z: left-right, positive right + + var command = { "ID": ID, "targetID": targetID, "offsetX": -50, "offsetY": -10, "offsetZ": 50}; var data = { "followUnit": command } POST(data, () => { }); } diff --git a/client/src/units/aircraftdatabase.ts b/client/src/units/aircraftdatabase.ts index 477d1f48..641400d2 100644 --- a/client/src/units/aircraftdatabase.ts +++ b/client/src/units/aircraftdatabase.ts @@ -708,6 +708,25 @@ export class AircraftDatabase extends UnitDatabase { ], "filename": "kc-135.png" }, + "KC135MPRS": { + "name": "KC135MPRS", + "label": "KC-135 MPRS", + "shortLabel": "135M", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Tanker" + ], + "code": "", + "name": "Default Tanker" + } + ], + "filename": "kc-135.png" + }, "MiG-23MLD": { "name": "MiG-23MLD", "label": "MiG-23MLD", diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index e570c0ec..ba30d161 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -515,6 +515,9 @@ export class Unit extends Marker { points.push(new LatLng(destination.lat, destination.lng)); this.#pathPolyline.setLatLngs(points); } + + if (points.length == 1) + this.#clearPath(); } } diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 0c087c0a..2f286da6 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -98,24 +98,46 @@ end -- Builds a valid task depending on the provided options function Olympus.buildTask(options) local task = nil - if options['id'] == 'FollowUnit' and options['leaderID'] and options['offset'] then - local leader = Olympus.getUnitByID(options['leaderID']) - if leader and leader:isExist() then + + if (Olympus.isArray(options)) then + local tasks = {} + for idx, subOptions in pairs(options) do + tasks[idx] = Olympus.buildTask(subOptions) or Olympus.buildEnrouteTask(subOptions) + end + task = { + id = 'ComboTask', + params = { + tasks = tasks + } + } + Olympus.debug(Olympus.serializeTable(task), 30) + else + if options['id'] == 'FollowUnit' and options['leaderID'] and options['offset'] then + local leader = Olympus.getUnitByID(options['leaderID']) + if leader and leader:isExist() then + task = { + id = 'Follow', + params = { + groupId = leader:getGroup():getID(), + pos = options['offset'], + lastWptIndexFlag = false, + lastWptIndex = 1 + } + } + end + elseif options['id'] == 'Refuel' then task = { - id = 'Follow', - params = { - groupId = leader:getGroup():getID(), - pos = options['offset'], - lastWptIndexFlag = false, - lastWptIndex = 1 - } + id = 'Refueling', + params = {} + } + elseif options['id'] == 'Orbit' then + task = { + id = 'Orbit', + params = { + pattern = options['pattern'] or "Circle" + } } end - elseif options['id'] == 'Refuel' then - task = { - id = 'Refueling', - params = {} - } end return task end @@ -269,6 +291,7 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, spawnOptions) ["payload"] = { ["pylons"] = payload, + ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, @@ -439,6 +462,16 @@ function Olympus.serializeTable(val, name, skipnewlines, depth) return tmp end +function Olympus.isArray(t) + local i = 0 + for _ in pairs(t) do + i = i + 1 + if t[i] == nil then return false end + end + return true +end + + function Olympus.setMissionData(arg, time) local missionData = {} diff --git a/src/core/include/airunit.h b/src/core/include/airunit.h index 550bbccf..a2aeb7a6 100644 --- a/src/core/include/airunit.h +++ b/src/core/include/airunit.h @@ -26,5 +26,4 @@ protected: void createHoldingPattern(); bool updateActivePath(bool looping); void goToDestination(wstring enrouteTask = L"nil"); - void taskWingmen(); }; \ No newline at end of file diff --git a/src/core/src/airunit.cpp b/src/core/src/airunit.cpp index 00214acc..cf3c9079 100644 --- a/src/core/src/airunit.cpp +++ b/src/core/src/airunit.cpp @@ -37,11 +37,6 @@ void AirUnit::setState(int newState) case State::FOLLOW: { break; } - case State::WINGMAN: { - if (isWingman) - return; - break; - } case State::LAND: { break; } @@ -55,6 +50,7 @@ void AirUnit::setState(int newState) /* Perform any action required when ENTERING a certain state */ switch (newState) { case State::IDLE: { + clearActivePath(); resetActiveDestination(); break; } @@ -73,10 +69,7 @@ void AirUnit::setState(int newState) break; } case State::FOLLOW: { - resetActiveDestination(); - break; - } - case State::WINGMAN: { + clearActivePath(); resetActiveDestination(); break; } @@ -180,71 +173,40 @@ void AirUnit::goToDestination(wstring enrouteTask) log(unitName + L" error, no active destination!"); } -void AirUnit::taskWingmen() -{ - switch (state) { - case State::IDLE: - case State::REACH_DESTINATION: - case State::ATTACK:{ - int idx = 1; - for (auto const& wingman : wingmen) - { - if (!wingman->getIsWingman()) - { - wingman->setIsWingman(true); - wingman->setLeader(this); - } - - if (wingman->getFormation().compare(formation) != 0) - { - wingman->resetTask(); - wingman->setFormation(formation); - if (formation.compare(L"Line abreast") == 0) - wingman->setFormationOffset(Offset(0 * idx, 0 * idx, 1852 * idx)); - idx++; - } - } - break; - } - default: - break; - } -} - void AirUnit::AIloop() { /* State machine */ switch (state) { case State::IDLE: { - wstring enrouteTask = L"nil"; currentTask = L"Idle"; - if (activeDestination == NULL || !hasTask) + if (!hasTask) { - createHoldingPattern(); - setActiveDestination(); - goToDestination(enrouteTask); + std::wostringstream taskSS; + if (isTanker) { + taskSS << "{ [1] = { id = 'Orbit', pattern = 'Race-Track' }, [2] = { id = 'Tanker' } }"; + } + else { + taskSS << "{ id = 'Orbit', pattern = 'Circle' }"; + } + Command* command = dynamic_cast(new SetTask(ID, taskSS.str())); + scheduler->appendCommand(command); + hasTask = true; } - else { - if (isDestinationReached() && updateActivePath(true) && setActiveDestination()) - goToDestination(enrouteTask); - } - - if (isLeader) - taskWingmen(); break; } case State::REACH_DESTINATION: { wstring enrouteTask = L""; + bool looping = false; if (isTanker) { - enrouteTask = L"{" "id = 'Tanker' }"; + enrouteTask = L"{ id = 'Tanker' }"; currentTask = L"Tanker"; } else if (isAWACS) { - enrouteTask = L"{" "id = 'AWACS' }"; + enrouteTask = L"{ id = 'AWACS' }"; currentTask = L"AWACS"; } else @@ -260,21 +222,16 @@ void AirUnit::AIloop() } else { if (isDestinationReached()) { - if (updateActivePath(false) && setActiveDestination()) + if (updateActivePath(looping) && setActiveDestination()) goToDestination(enrouteTask); else { setState(State::IDLE); break; } } - } - - if (isLeader) - taskWingmen(); - + } break; } - case State::LAND: { wstring enrouteTask = L"{" "id = 'land' }"; currentTask = L"Landing"; @@ -284,10 +241,6 @@ void AirUnit::AIloop() setActiveDestination(); goToDestination(enrouteTask); } - - if (isLeader) - taskWingmen(); - break; } case State::ATTACK: { @@ -322,34 +275,28 @@ void AirUnit::AIloop() } } } - - if (isLeader) - taskWingmen(); - break; } case State::FOLLOW: { - /* TODO */ - setState(State::IDLE); - break; - } - case State::WINGMAN: { - /* In the WINGMAN state, the unit relinquishes control to the leader */ clearActivePath(); activeDestination = Coords(NULL); - if (leader == nullptr || !leader->getAlive()) - { - this->setFormation(L""); - this->setIsWingman(false); + + /* If the target is not alive (either not set or was destroyed) go back to IDLE */ + if (!isTargetAlive()) { + setState(State::IDLE); break; } + + currentTask = L"Following " + getTargetName(); + + Unit* target = unitsManager->getUnit(targetID); if (!hasTask) { - if (leader != nullptr && leader->getAlive() && formationOffset != NULL) + if (target != nullptr && target->getAlive() && formationOffset != NULL) { std::wostringstream taskSS; taskSS << "{" << "id = 'FollowUnit'" << ", " - << "leaderID = " << leader->getID() << "," + << "leaderID = " << target->getID() << "," << "offset = {" << "x = " << formationOffset.x << "," << "y = " << formationOffset.y << "," @@ -364,6 +311,8 @@ void AirUnit::AIloop() break; } case State::REFUEL: { + currentTask = L"Refueling"; + if (!hasTask) { std::wostringstream taskSS; taskSS << "{" @@ -377,4 +326,6 @@ void AirUnit::AIloop() default: break; } + addMeasure(L"currentTask", json::value(currentTask)); + addMeasure(L"currentState", json::value(state)); } diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp index 61dc7507..318945f4 100644 --- a/src/core/src/scheduler.cpp +++ b/src/core/src/scheduler.cpp @@ -142,14 +142,34 @@ void Scheduler::handleRequest(wstring key, json::value value) unit->setTargetID(targetID); unit->setState(State::ATTACK); } - else if (key.compare(L"stopAttack") == 0) + else if (key.compare(L"followUnit") == 0) { int ID = value[L"ID"].as_integer(); + int targetID = value[L"targetID"].as_integer(); + int offsetX = value[L"offsetX"].as_integer(); + int offsetY = value[L"offsetY"].as_integer(); + int offsetZ = value[L"offsetZ"].as_integer(); + Unit* unit = unitsManager->getUnit(ID); + Unit* target = unitsManager->getUnit(targetID); + + wstring unitName; + wstring targetName; + if (unit != nullptr) - unit->setState(State::REACH_DESTINATION); + unitName = unit->getUnitName(); else return; + + if (target != nullptr) + targetName = target->getUnitName(); + else + return; + + log(L"Unit " + unitName + L" following unit " + targetName); + unit->setFormationOffset(Offset(offsetX, offsetY, offsetZ)); + unit->setTargetID(targetID); + unit->setState(State::FOLLOW); } else if (key.compare(L"changeSpeed") == 0) { diff --git a/src/core/src/unit.cpp b/src/core/src/unit.cpp index 815af80b..ee64b6c3 100644 --- a/src/core/src/unit.cpp +++ b/src/core/src/unit.cpp @@ -134,7 +134,7 @@ json::value Unit::getData(long long time) /********** Task data **********/ json[L"taskData"] = json::value::object(); - for (auto key : { L"currentTask", L"targetSpeed", L"targetAltitude", L"activePath" }) + for (auto key : { L"currentTask", L"targetSpeed", L"targetAltitude", L"activePath", L"isTanker", L"isAWACS", L"TACANChannel", L"TACANXY", L"TACANCallsign", L"radioFrequency", L"radioCallsign", L"radioCallsignNumber"}) { if (measures.find(key) != measures.end() && measures[key]->getTime() > time) json[L"taskData"][key] = measures[key]->getValue(); diff --git a/src/dcstools/src/dcstools.cpp b/src/dcstools/src/dcstools.cpp index 1af8ebf2..64184a98 100644 --- a/src/dcstools/src/dcstools.cpp +++ b/src/dcstools/src/dcstools.cpp @@ -105,6 +105,6 @@ int dostring_in(lua_State* L, string target, string command) int TACANChannelToFrequency(int channel, wstring XY) { - int basef = (XY == L"X" && channel > 63) || (XY == L"Y" && channel < 64) ? 1087000000 : 961000000; - return basef + 1000000 * channel; + int basef = (XY == L"X" && channel > 63) || (XY == L"Y" && channel < 64) ? 1087: 961; + return (basef + channel) * 1000000; } \ No newline at end of file