diff --git a/client/app.js b/client/app.js index 85d5c31d..651c2b73 100644 --- a/client/app.js +++ b/client/app.js @@ -3,7 +3,6 @@ var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var fs = require('fs'); -var basicAuth = require('express-basic-auth') var atcRouter = require('./routes/api/atc'); var airbasesRouter = require('./routes/api/airbases'); @@ -37,15 +36,7 @@ if (config["server"] != undefined) module.exports = app; const DemoDataGenerator = require('./demo.js'); -var demoDataGenerator = new DemoDataGenerator(10); -app.get('/demo/units', (req, res) => demoDataGenerator.units(req, res)); -app.get('/demo/logs', (req, res) => demoDataGenerator.logs(req, res)); -app.get('/demo/bullseyes', (req, res) => demoDataGenerator.bullseyes(req, res)); -app.get('/demo/airbases', (req, res) => demoDataGenerator.airbases(req, res)); -app.get('/demo/mission', (req, res) => demoDataGenerator.mission(req, res)); - -app.use('/demo', basicAuth({ - users: { 'admin': 'socks' } -})) +var demoDataGenerator = new DemoDataGenerator(app); + diff --git a/client/demo.js b/client/demo.js index be34fce8..8c515a85 100644 --- a/client/demo.js +++ b/client/demo.js @@ -1,7 +1,8 @@ +var basicAuth = require('express-basic-auth') var enc = new TextEncoder(); const DEMO_UNIT_DATA = { - ["1"]:{ alive: true, human: false, controlled: true, coalition: 2, country: 0, name: "KC-135", unitName: "Cool guy 1-1", groupName: "Cool group 1", state: 3, task: "Being cool!", + ["1"]:{ category: "Aircraft", alive: true, human: false, controlled: true, coalition: 2, country: 0, name: "KC-135", unitName: "Cool guy 1-1 who also has a very long name", groupName: "Cool group 1", state: 3, task: "Being cool!", hasTask: true, position: { lat: 37, lng: -116, alt: 1000 }, speed: 200, heading: 45, isTanker: true, isAWACS: false, onOff: true, followRoads: false, fuel: 50, desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0, formationOffset: { x: 0, y: 0, z: 0 }, @@ -14,15 +15,15 @@ const DEMO_UNIT_DATA = { radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 }, generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false }, ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ], - contacts: [], - activePath: [ {lat: 38, lng: -115, alt: 0}, {lat: 38, lng: -114, alt: 0} ] + contacts: [{ID: 2, detectionMethod: 1}], + activePath: [{lat: 38, lng: -115, alt: 0}, {lat: 38, lng: -114, alt: 0}] }, - ["2"]:{ alive: true, human: false, controlled: false, coalition: 1, country: 0, name: "KC-135", unitName: "Cool guy 1-2", groupName: "Cool group 2", state: 1, task: "Being cool", - hasTask: false, position: { lat: 36.9, lng: -116, alt: 1000 }, speed: 200, heading: 0, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50, + ["2"]:{ category: "Aircraft", alive: true, human: false, controlled: false, coalition: 1, country: 0, name: "FA-18C_hornet", unitName: "Cool guy 1-2", groupName: "Cool group 2", state: 1, task: "Being cool", + hasTask: false, position: { lat: 36.9, lng: -116, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50, desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0, formationOffset: { x: 0, y: 0, z: 0 }, targetID: 0, - targetPosition: { lat: 38, lng: -117, alt: 1000 }, + targetPosition: { lat: 0, lng: 0, alt: 0 }, ROE: 2, reactionToThreat: 1, emissionsCountermeasures: 1, @@ -30,15 +31,57 @@ const DEMO_UNIT_DATA = { radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 }, generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false }, ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ], - contacts: [{ID: 1, detectionMethod: 4}], - activePath: [ {lat: 38, lng: -115, alt: 0}, {lat: 38, lng: -114, alt: 0} ] + contacts: [{ID: 1, detectionMethod: 16}], + activePath: [ ] + }, ["3"]:{ category: "GroundUnit", alive: true, human: false, controlled: false, coalition: 1, country: 0, name: "M-60", unitName: "Cool guy 1-3", groupName: "Cool group 3", state: 1, task: "Being cool", + hasTask: false, position: { lat: 37.1, lng: -116, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50, + desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0, + formationOffset: { x: 0, y: 0, z: 0 }, + targetID: 0, + targetPosition: { lat: 0, lng: 0, alt: 0 }, + ROE: 2, + reactionToThreat: 1, + emissionsCountermeasures: 1, + TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, + radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 }, + generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false }, + ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ], + contacts: [{ID: 1, detectionMethod: 16}], + activePath: [ ] + }, ["4"]:{ category: "GroundUnit", alive: true, human: false, controlled: false, coalition: 1, country: 0, name: "M-60", unitName: "Cool guy 1-4", groupName: "Cool group 3", state: 1, task: "Being cool", + hasTask: false, position: { lat: 37.1, lng: -116.1, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50, + desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0, + formationOffset: { x: 0, y: 0, z: 0 }, + targetID: 0, + targetPosition: { lat: 0, lng: 0, alt: 0 }, + ROE: 2, + reactionToThreat: 1, + emissionsCountermeasures: 1, + TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, + radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 }, + generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false }, + ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ], + contacts: [{ID: 1, detectionMethod: 16}], + activePath: [ ] } } class DemoDataGenerator { - constructor() + constructor(app) { - + app.get('/demo/units', (req, res) => this.units(req, res)); + app.get('/demo/logs', (req, res) => this.logs(req, res)); + 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.use('/demo', basicAuth({ + users: { + 'admin': 'socks', + 'blue': 'bluesocks', + 'red': 'redsocks' + }, + })) } units(req, res){ @@ -48,7 +91,7 @@ class DemoDataGenerator { for (let idx in DEMO_UNIT_DATA) { const unit = DEMO_UNIT_DATA[idx]; array = this.concat(array, this.uint32ToByteArray(idx)); - array = this.appendString(array, "Aircraft", 1); + array = this.appendString(array, unit.category, 1); array = this.appendUint8(array, unit.alive, 2); array = this.appendUint8(array, unit.human, 3); array = this.appendUint8(array, unit.controlled, 4); @@ -302,6 +345,21 @@ class DemoDataGenerator { mission(req, res){ var ret = {mission: {theatre: "Nevada"}}; ret.time = Date.now(); + var auth = req.get("Authorization"); + if (auth) { + var username = atob(auth.replace("Basic ", "")).split(":")[0]; + switch (username) { + case "admin": + ret.mission.visibilityMode = "Game master"; + break + case "blue": + ret.mission.visibilityMode = "Blue commander"; + break; + case "red": + ret.mission.visibilityMode = "Red commander"; + break; + } + } res.send(JSON.stringify(ret)); } diff --git a/client/public/stylesheets/markers/units.css b/client/public/stylesheets/markers/units.css index 62ca82b6..4cbdcc90 100644 --- a/client/public/stylesheets/markers/units.css +++ b/client/public/stylesheets/markers/units.css @@ -150,7 +150,7 @@ 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; - translate: -60px 0; + right: 100%; width: fit-content; } @@ -171,7 +171,7 @@ width: 80px; } -[data-object|="unit"] .unit-summary .unit-callsign:hover { +[data-object|="unit"]:hover .unit-summary .unit-callsign{ direction: rtl; overflow: visible; } diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 480d5ab7..58b587cd 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -812,16 +812,16 @@ nav.ol-panel> :last-child { font-weight: bold; } -#connection-status { +#login-status { margin-bottom: 5px; } -#connection-status[data-status="connecting"]::before { +#login-status[data-status="connecting"]::before { animation: blinker 1s linear infinite; content: "Connecting..."; } -#connection-status[data-status="failed"]::before { +#login-status[data-status="failed"]::before { color: var(--primary-red); content: "Incorrect username/password!"; } @@ -865,6 +865,19 @@ nav.ol-panel> :last-child { translate: 0% -300%; } +#visibiliy-mode { + font-size: 14px; + font-weight: bolder; +} + +#visibiliy-mode[data-mode="Blue commander"] { + color: var(--primary-blue); +} + +#visibiliy-mode[data-mode="Red commander"] { + color: var(--primary-red); +} + .ol-destination-preview-icon { background-image: url("/resources/theme/images/markers/move.svg"); height: 52px; diff --git a/client/src/@types/dom.d.ts b/client/src/@types/dom.d.ts index 3feba2b0..9d260bb8 100644 --- a/client/src/@types/dom.d.ts +++ b/client/src/@types/dom.d.ts @@ -17,7 +17,8 @@ interface CustomEventMap { "groupCreation": CustomEvent, "groupDeletion": CustomEvent, "mapStateChanged": CustomEvent, - "mapContextMenu": CustomEvent<> + "mapContextMenu": CustomEvent<>, + "visibilityModeChanged": CustomEvent, } declare global { diff --git a/client/src/@types/server.d.ts b/client/src/@types/server.d.ts index 23920339..1f17211d 100644 --- a/client/src/@types/server.d.ts +++ b/client/src/@types/server.d.ts @@ -1,8 +1,3 @@ -interface UnitsData { - units: string, - sessionHash: string -} - interface AirbasesData { airbases: {[key: string]: any}, } diff --git a/client/src/@types/unit.d.ts b/client/src/@types/unit.d.ts index ec6aaa39..4026b637 100644 --- a/client/src/@types/unit.d.ts +++ b/client/src/@types/unit.d.ts @@ -8,7 +8,8 @@ interface UnitIconOptions { showShortLabel: boolean, showFuel: boolean, showAmmo: boolean, - showSummary: boolean, + showSummary: boolean, + showCallsign: boolean, rotateToHeading: boolean } diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index aa9c1189..b0c300f1 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -1,4 +1,16 @@ -import { LatLng, LatLngBounds, TileLayer, tileLayer } from "leaflet"; +import { LatLng, LatLngBounds } from "leaflet"; + +export const HIDE_ALL = "Hide all"; +export const GAME_MASTER = "Game master"; +export const BLUE_COMMANDER = "Blue commander"; +export const RED_COMMANDER = "Red commander"; + +export const VISUAL = 1; +export const OPTIC = 2; +export const RADAR = 4; +export const IRST = 8; +export const RWR = 16; +export const DLINK = 32; export const states: string[] = ["none", "idle", "reach-destination", "attack", "follow", "land", "refuel", "AWACS", "tanker", "bomb-point", "carpet-bomb", "bomb-building", "fire-at-area"]; export const ROEs: string[] = ["free", "designated", "return", "hold"]; diff --git a/client/src/controls/mapcontextmenu.ts b/client/src/controls/mapcontextmenu.ts index c0cdcdf9..8db22841 100644 --- a/client/src/controls/mapcontextmenu.ts +++ b/client/src/controls/mapcontextmenu.ts @@ -1,6 +1,6 @@ import { LatLng } from "leaflet"; import { getActiveCoalition, getMap, setActiveCoalition } from ".."; -import { spawnAircraft, spawnExplosion, spawnGroundUnit, spawnSmoke } from "../server/server"; +import { spawnAircrafts, spawnExplosion, spawnGroundUnits, spawnSmoke } from "../server/server"; import { aircraftDatabase } from "../units/aircraftdatabase"; import { groundUnitsDatabase } from "../units/groundunitsdatabase"; import { ContextMenu } from "./contextmenu"; @@ -9,17 +9,6 @@ import { Switch } from "./switch"; import { Slider } from "./slider"; import { ftToM } from "../other/utils"; -export interface SpawnOptions { - role: string; - name: string; - latlng: LatLng; - coalition: string; - loadout?: string | null; - airbaseName?: string | null; - altitude?: number | null; - immediate?: boolean; -} - export class MapContextMenu extends ContextMenu { #coalitionSwitch: Switch; #aircraftRoleDropdown: Dropdown; @@ -28,7 +17,7 @@ export class MapContextMenu extends ContextMenu { #aircrafSpawnAltitudeSlider: Slider; #groundUnitRoleDropdown: Dropdown; #groundUnitTypeDropdown: Dropdown; - #spawnOptions: SpawnOptions = { role: "", name: "", latlng: new LatLng(0, 0), loadout: null, coalition: "blue", airbaseName: null, altitude: ftToM(20000) }; + #spawnOptions = { role: "", name: "", latlng: new LatLng(0, 0), coalition: "blue", loadout: "", airbaseName: "", altitude: ftToM(20000) }; constructor(id: string) { super(id); @@ -57,8 +46,8 @@ export class MapContextMenu extends ContextMenu { this.hide(); this.#spawnOptions.coalition = getActiveCoalition(); if (this.#spawnOptions) { - getMap().addTemporaryMarker(this.#spawnOptions); - spawnAircraft(this.#spawnOptions); + getMap().addTemporaryMarker(this.#spawnOptions.latlng, this.#spawnOptions.name, getActiveCoalition()); + spawnAircrafts([{unitName: this.#spawnOptions.name, latlng: this.#spawnOptions.latlng, loadout: this.#spawnOptions.loadout}], getActiveCoalition(), this.#spawnOptions.airbaseName, false); } }); @@ -66,8 +55,8 @@ export class MapContextMenu extends ContextMenu { this.hide(); this.#spawnOptions.coalition = getActiveCoalition(); if (this.#spawnOptions) { - getMap().addTemporaryMarker(this.#spawnOptions); - spawnGroundUnit(this.#spawnOptions); + getMap().addTemporaryMarker(this.#spawnOptions.latlng, this.#spawnOptions.name, getActiveCoalition()); + spawnGroundUnits([{unitName: this.#spawnOptions.name, latlng: this.#spawnOptions.latlng}], getActiveCoalition(), false); } }); @@ -86,7 +75,7 @@ export class MapContextMenu extends ContextMenu { } show(x: number, y: number, latlng: LatLng) { - this.#spawnOptions.airbaseName = null; + this.#spawnOptions.airbaseName = ""; super.show(x, y, latlng); this.#spawnOptions.latlng = latlng; this.showUpperBar(); diff --git a/client/src/index.ts b/client/src/index.ts index 933551cb..b86827f7 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -184,12 +184,12 @@ function setupEvents() { const form = document.querySelector("#splash-content")?.querySelector("#authentication-form"); const username = ((form?.querySelector("#username"))).value; const password = ((form?.querySelector("#password"))).value; - setCredentials(username, btoa("admin" + ":" + password)); + setCredentials(username, password); /* Start periodically requesting updates */ startUpdate(); - setConnectionStatus("connecting"); + setLoginStatus("connecting"); }) document.addEventListener("reloadPage", () => { @@ -259,8 +259,8 @@ export function getActiveCoalition() { return activeCoalition; } -export function setConnectionStatus(status: string) { - const el = document.querySelector("#connection-status") as HTMLElement; +export function setLoginStatus(status: string) { + const el = document.querySelector("#login-status") as HTMLElement; if (el) el.dataset["status"] = status; } diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 18c74d99..06cc58c2 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -1,7 +1,7 @@ import * as L from "leaflet" import { getUnitsManager } from ".."; import { BoxSelect } from "./boxselect"; -import { MapContextMenu, SpawnOptions } from "../controls/mapcontextmenu"; +import { MapContextMenu } from "../controls/mapcontextmenu"; import { UnitContextMenu } from "../controls/unitcontextmenu"; import { AirbaseContextMenu } from "../controls/airbasecontextmenu"; import { Dropdown } from "../controls/dropdown"; @@ -390,8 +390,8 @@ export class Map extends L.Map { } } - addTemporaryMarker(spawnOptions: SpawnOptions) { - var marker = new TemporaryUnitMarker(spawnOptions); + addTemporaryMarker(latlng: L.LatLng, name: string, coalition: string) { + var marker = new TemporaryUnitMarker(latlng, name, coalition); marker.addTo(this); this.#temporaryMarkers.push(marker); } diff --git a/client/src/map/temporaryunitmarker.ts b/client/src/map/temporaryunitmarker.ts index 7c266e0e..81136e6a 100644 --- a/client/src/map/temporaryunitmarker.ts +++ b/client/src/map/temporaryunitmarker.ts @@ -1,19 +1,20 @@ import { CustomMarker } from "./custommarker"; -import { SpawnOptions } from "../controls/mapcontextmenu"; -import { DivIcon } from "leaflet"; +import { DivIcon, LatLng } from "leaflet"; import { SVGInjector } from "@tanem/svg-injector"; import { getMarkerCategoryByName, getUnitDatabaseByCategory } from "../other/utils"; export class TemporaryUnitMarker extends CustomMarker { - #spawnOptions: SpawnOptions; + #name: string; + #coalition: string; - constructor(spawnOptions: SpawnOptions) { - super(spawnOptions.latlng, {interactive: false}); - this.#spawnOptions = spawnOptions; + constructor(latlng: LatLng, name: string, coalition: string) { + super(latlng, {interactive: false}); + this.#name = name; + this.#coalition = coalition; } createIcon() { - const category = getMarkerCategoryByName(this.#spawnOptions.name); + const category = getMarkerCategoryByName(this.#name); /* Set the icon */ var icon = new DivIcon({ @@ -26,7 +27,7 @@ export class TemporaryUnitMarker extends CustomMarker { var el = document.createElement("div"); el.classList.add("unit"); el.setAttribute("data-object", `unit-${category}`); - el.setAttribute("data-coalition", this.#spawnOptions.coalition); + el.setAttribute("data-coalition", this.#coalition); // Main icon var unitIcon = document.createElement("div"); @@ -42,7 +43,7 @@ export class TemporaryUnitMarker extends CustomMarker { if (category == "aircraft" || category == "helicopter") { var shortLabel = document.createElement("div"); shortLabel.classList.add("unit-short-label"); - shortLabel.innerText = getUnitDatabaseByCategory(category)?.getByName(this.#spawnOptions.name)?.shortLabel || ""; + shortLabel.innerText = getUnitDatabaseByCategory(category)?.getByName(this.#name)?.shortLabel || ""; el.append(shortLabel); } diff --git a/client/src/missionhandler/missionhandler.ts b/client/src/missionhandler/missionhandler.ts index 0c81884b..cac749f1 100644 --- a/client/src/missionhandler/missionhandler.ts +++ b/client/src/missionhandler/missionhandler.ts @@ -1,5 +1,5 @@ import { LatLng } from "leaflet"; -import { getInfoPopup, getMap } from ".."; +import { getInfoPopup, getMap, getUnitsManager } from ".."; import { Airbase } from "./airbase"; import { Bullseye } from "./bullseye"; @@ -72,6 +72,9 @@ export class MissionHandler { getInfoPopup().setText("Map set to " + this.#theatre); } + if ("visibilityMode" in data.mission) + getUnitsManager().setVisibilityMode(data.mission.visibilityMode); + if ("date" in data.mission) this.#date = data.mission.date; if ("elapsedTime" in data.mission) diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts index 39f95722..0fd69911 100644 --- a/client/src/other/utils.ts +++ b/client/src/other/utils.ts @@ -35,6 +35,16 @@ export function distance(lat1: number, lon1: number, lat2: number, lon2: number) return d; } +export function coordinatesFromBearingAndDistance(lat1: number, lon1: number, brng: number, dist: number) { + const R = 6371e3; // metres + const φ1 = deg2rad(lat1); // φ, λ in radians + const λ1 = deg2rad(lon1); + const φ2 = Math.asin( Math.sin(φ1)*Math.cos(dist/R) + Math.cos(φ1)*Math.sin(dist/R)*Math.cos(brng) ); + const λ2 = λ1 + Math.atan2(Math.sin(brng)*Math.sin(dist/R)*Math.cos(φ1), Math.cos(dist/R)-Math.sin(φ1)*Math.sin(φ2)); + + return {lat: rad2deg(φ2), lng: rad2deg(λ2)}; +} + export function ConvertDDToDMS(D: number, lng: boolean) { var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N"; var deg = 0 | (D < 0 ? (D = -D) : D); diff --git a/client/src/server/server.ts b/client/src/server/server.ts index 533053fe..fefee15f 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -1,6 +1,5 @@ import { LatLng } from 'leaflet'; -import { getConnectionStatusPanel, getInfoPopup, getMissionData, getUnitDataTable, getUnitsManager, setConnectionStatus } from '..'; -import { SpawnOptions } from '../controls/mapcontextmenu'; +import { getConnectionStatusPanel, getInfoPopup, getMissionData, getUnitDataTable, getUnitsManager, setLoginStatus } from '..'; import { GeneralSettings, Radio, TACAN } from '../@types/unit'; import { ROEs, emissionsCountermeasures, reactionsToThreat } from '../constants/constants'; @@ -16,7 +15,7 @@ const BULLSEYE_URI = "bullseyes"; const MISSION_URI = "mission"; var username = ""; -var credentials = ""; +var password = ""; var sessionHash: string | null = null; var lastUpdateTime = 0; @@ -26,12 +25,12 @@ export function toggleDemoEnabled() { demoEnabled = !demoEnabled; } -export function setCredentials(newUsername: string, newCredentials: string) { +export function setCredentials(newUsername: string, newPassword: string) { username = newUsername; - credentials = newCredentials; + password = newPassword; } -export function GET(callback: CallableFunction, uri: string, options?: { time?: number }) { +export function GET(callback: CallableFunction, uri: string, options?: { time?: number }, responseType?: string) { var xmlHttp = new XMLHttpRequest(); /* Assemble the request options string */ @@ -39,26 +38,31 @@ export function GET(callback: CallableFunction, uri: string, options?: { time?: if (options?.time != undefined) optionsString = `time=${options.time}`; + /* On the connection */ xmlHttp.open("GET", `${demoEnabled ? DEMO_ADDRESS : REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ''}`, true); - if (credentials) - xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); - if (uri === UNITS_URI) - xmlHttp.responseType = "arraybuffer"; + /* If provided, set the credentials */ + if (username && password) + xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${username}:${password}`)); + + /* If specified, set the response type */ + if (responseType) + xmlHttp.responseType = responseType as XMLHttpRequestResponseType; xmlHttp.onload = function (e) { if (xmlHttp.status == 200) { + /* Success */ setConnected(true); if (xmlHttp.responseType == 'arraybuffer') callback(xmlHttp.response); - else { - var data = JSON.parse(xmlHttp.responseText); - callback(data); - } + else + callback(JSON.parse(xmlHttp.responseText)); } else if (xmlHttp.status == 401) { + /* Bad credentials */ console.error("Incorrect username/password"); - setConnectionStatus("failed"); + setLoginStatus("failed"); } else { + /* Failure, probably disconnected */ setConnected(false); } }; @@ -73,8 +77,8 @@ export function POST(request: object, callback: CallableFunction) { var xmlHttp = new XMLHttpRequest(); xmlHttp.open("PUT", demoEnabled ? DEMO_ADDRESS : REST_ADDRESS); xmlHttp.setRequestHeader("Content-Type", "application/json"); - if (credentials) - xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); + if (username && password) + xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${username}:${password}`)); xmlHttp.onreadystatechange = () => { callback(); }; @@ -120,7 +124,7 @@ export function getMission(callback: CallableFunction) { } export function getUnits(callback: CallableFunction, refresh: boolean = false) { - GET(callback, `${UNITS_URI}`, { time: refresh ? 0 : lastUpdateTime }); + GET(callback, `${UNITS_URI}`, { time: refresh ? 0 : lastUpdateTime }, 'arraybuffer'); } export function addDestination(ID: number, path: any) { @@ -141,15 +145,15 @@ export function spawnExplosion(intensity: number, latlng: LatLng) { POST(data, () => { }); } -export function spawnGroundUnit(spawnOptions: SpawnOptions) { - var command = { "type": spawnOptions.name, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "immediate": spawnOptions.immediate? true: false }; - var data = { "spawnGround": command } +export function spawnGroundUnits(units: any, coalition: string, immediate: boolean) { + var command = { "units": units, "coalition": coalition, "immediate": immediate }; + var data = { "spawnGroundUnits": command } POST(data, () => { }); } -export function spawnAircraft(spawnOptions: SpawnOptions) { - var command = { "type": spawnOptions.name, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "altitude": spawnOptions.altitude, "payloadName": spawnOptions.loadout != null ? spawnOptions.loadout : "", "airbaseName": spawnOptions.airbaseName != null ? spawnOptions.airbaseName : "", "immediate": spawnOptions.immediate? true: false }; - var data = { "spawnAir": command } +export function spawnAircrafts(units: any, coalition: string, airbaseName: string, immediate: boolean) { + var command = { "units": units, "coalition": coalition, "airbaseName": airbaseName, "immediate": immediate }; + var data = { "spawnAircrafts": command } POST(data, () => { }); } @@ -319,11 +323,11 @@ export function startUpdate() { export function requestUpdate() { /* Main update rate = 250ms is minimum time, equal to server update time. */ - getUnits((buffer: ArrayBuffer) => { - if (!getPaused()) { + if (!getPaused()) { + getUnits((buffer: ArrayBuffer) => { getUnitsManager()?.update(buffer); - } - }, false); + }, false); + } window.setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); getConnectionStatusPanel()?.update(getConnected()); diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index 05b8772a..ee7be24f 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -1,12 +1,12 @@ -import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map } from 'leaflet'; +import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point } from 'leaflet'; import { getMap, getUnitsManager } from '..'; -import { enumToCoalition, enumToEmissioNCountermeasure, getMarkerCategoryByName, enumToROE, enumToReactionToThreat, enumToState, getUnitDatabaseByCategory, mToFt, msToKnots, rad2deg } from '../other/utils'; +import { enumToCoalition, enumToEmissioNCountermeasure, getMarkerCategoryByName, enumToROE, enumToReactionToThreat, enumToState, getUnitDatabaseByCategory, mToFt, msToKnots, rad2deg, bearing, coordinatesFromBearingAndDistance, deg2rad } from '../other/utils'; import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit, setEmissionsCountermeasures, setSpeedType, setAltitudeType, setOnOff, setFollowRoads, bombPoint, carpetBomb, bombBuilding, fireAtArea } from '../server/server'; import { CustomMarker } from '../map/custommarker'; import { SVGInjector } from '@tanem/svg-injector'; import { UnitDatabase } from './unitdatabase'; import { TargetMarker } from '../map/targetmarker'; -import { BOMBING, CARPET_BOMBING, DataIndexes, FIRE_AT_AREA, IDLE, MOVE_UNIT, ROEs, emissionsCountermeasures, reactionsToThreat, states } from '../constants/constants'; +import { BLUE_COMMANDER, BOMBING, CARPET_BOMBING, DLINK, DataIndexes, FIRE_AT_AREA, HIDE_ALL, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, RED_COMMANDER, ROEs, RWR, VISUAL, emissionsCountermeasures, reactionsToThreat, states } from '../constants/constants'; import { Ammo, Contact, GeneralSettings, Offset, Radio, TACAN, UnitIconOptions } from '../@types/unit'; import { DataExtractor } from './dataextractor'; @@ -88,6 +88,7 @@ export class Unit extends CustomMarker { #targetPositionPolyline: Polyline; #timer: number = 0; #hotgroup: number | null = null; + #detectionMethods: number[] = []; getAlive() {return this.#alive}; getHuman() {return this.#human}; @@ -296,7 +297,8 @@ export class Unit extends CustomMarker { showShortLabel: false, showFuel: false, showAmmo: false, - showSummary: false, + showSummary: true, + showCallsign: true, rotateToHeading: false } } @@ -309,7 +311,7 @@ export class Unit extends CustomMarker { setSelected(selected: boolean) { /* Only alive units can be selected. Some units are not selectable (weapons) */ - if ((this.#alive || !selected) && this.getSelectable() && this.getSelected() != selected) { + if ((this.#alive || !selected) && this.getSelectable() && this.getSelected() != selected && this.belongsToCommandedCoalition()) { this.#selected = selected; this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-selected", selected); if (selected) { @@ -362,6 +364,16 @@ export class Unit extends CustomMarker { return Object.values(getUnitsManager().getUnits()).filter((unit: Unit) => { return unit != this && unit.#groupName === this.#groupName; }); } + belongsToCommandedCoalition() { + if (getUnitsManager().getVisibilityMode() === HIDE_ALL) + return false; + if (getUnitsManager().getVisibilityMode() === BLUE_COMMANDER && this.#coalition !== "blue") + return false; + if (getUnitsManager().getVisibilityMode() === RED_COMMANDER && this.#coalition !== "red") + return false; + return true; + } + /********************** Icon *************************/ createIcon(): void { /* Set the icon */ @@ -453,7 +465,7 @@ export class Unit extends CustomMarker { altitude.classList.add("unit-altitude"); var speed = document.createElement("div"); speed.classList.add("unit-speed"); - summary.appendChild(callsign); + if (this.getIconOptions().showCallsign) summary.appendChild(callsign); summary.appendChild(altitude); summary.appendChild(speed); el.appendChild(summary); @@ -468,12 +480,17 @@ export class Unit extends CustomMarker { const hiddenUnits = getUnitsManager().getHiddenTypes(); if (this.#human && hiddenUnits.includes("human")) hidden = true; - else if (this.#controlled == false && hiddenUnits.includes("dcs")) + if (this.#controlled == false && hiddenUnits.includes("dcs")) hidden = true; - else if (hiddenUnits.includes(this.getMarkerCategory())) + if (hiddenUnits.includes(this.getMarkerCategory())) hidden = true; - else if (hiddenUnits.includes(this.#coalition)) + if (hiddenUnits.includes(this.#coalition)) hidden = true; + if (getUnitsManager().getVisibilityMode() === HIDE_ALL) + hidden = true; + if (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0) { + hidden = true; + } this.setHidden(hidden || !this.#alive); } @@ -495,6 +512,22 @@ export class Unit extends CustomMarker { return this.#hidden; } + setDetectionMethods(newDetectionMethods: number[]) { + if (!this.belongsToCommandedCoalition()) { + /* Check if the detection methods of this unit have changed */ + if (this.#detectionMethods.length !== newDetectionMethods.length || this.getDetectionMethods().some(value => !newDetectionMethods.includes(value))) { + /* Force a redraw of the unit to reflect the new status of the detection methods */ + this.setHidden(true); + this.#detectionMethods = newDetectionMethods; + this.updateVisibility(); + } + } + } + + getDetectionMethods() { + return this.#detectionMethods; + } + getLeader() { return getUnitsManager().getUnitByID(this.#leaderID); } @@ -912,19 +945,28 @@ export class Unit extends CustomMarker { var contactData = this.#contacts[index]; var contact = getUnitsManager().getUnitByID(contactData.ID) if (contact != null) { - var startLatLng = new LatLng(this.#position.lat, this.#position.lng) - var endLatLng = new LatLng(contact.#position.lat, contact.#position.lng) + var startLatLng = new LatLng(this.#position.lat, this.#position.lng); + var endLatLng: LatLng; + if (contactData.detectionMethod === RWR) { + var bearingToContact = bearing(this.#position.lat, this.#position.lng, contact.#position.lat, contact.#position.lng); + var startXY = getMap().latLngToContainerPoint(startLatLng); + var endX = startXY.x + 80 * Math.sin(deg2rad(bearingToContact)); + var endY = startXY.y - 80 * Math.cos(deg2rad(bearingToContact)); + endLatLng = getMap().containerPointToLatLng(new Point(endX, endY)); + } + else + endLatLng = new LatLng(contact.#position.lat, contact.#position.lng); var color; - if (contactData.detectionMethod === 1) + if (contactData.detectionMethod === VISUAL || contactData.detectionMethod === OPTIC) color = "#FF00FF"; - else if (contactData.detectionMethod === 4) + else if (contactData.detectionMethod === RADAR || contactData.detectionMethod === IRST) color = "#FFFF00"; - else if (contactData.detectionMethod === 16) + else if (contactData.detectionMethod === RWR) color = "#00FF00"; else color = "#FFFFFF"; - var contactPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 0.4, smoothFactor: 1, dashArray: "4, 8" }); + var contactPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 1, smoothFactor: 1, dashArray: "4, 8" }); contactPolyline.addTo(getMap()); this.#contactsPolylines.push(contactPolyline) } @@ -941,10 +983,11 @@ export class Unit extends CustomMarker { if (this.#targetPosition.lat != 0 && this.#targetPosition.lng != 0) { this.#drawtargetPosition(this.#targetPosition); } - else if (this.#targetID != 0 && getUnitsManager().getUnitByID(this.#targetID)) { - const position = getUnitsManager().getUnitByID(this.#targetID)?.getPosition(); - if (position) - this.#drawtargetPosition(position); + else if (this.#targetID != 0) { + const target = getUnitsManager().getUnitByID(this.#targetID); + if (target && getUnitsManager().getUnitDetectedMethods(target).some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))) { + this.#drawtargetPosition(target.getPosition()); + } } else this.#clearTarget(); @@ -971,14 +1014,15 @@ export class Unit extends CustomMarker { export class AirUnit extends Unit { getIconOptions() { return { - showState: true, - showVvi: true, - showHotgroup: true, - showUnitIcon: true, - showShortLabel: true, - showFuel: true, - showAmmo: true, - showSummary: true, + showState: this.belongsToCommandedCoalition(), + showVvi: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showHotgroup: this.belongsToCommandedCoalition(), + showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showShortLabel: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value))), + showFuel: this.belongsToCommandedCoalition(), + showAmmo: this.belongsToCommandedCoalition(), + showSummary: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showCallsign: this.belongsToCommandedCoalition(), rotateToHeading: false }; } @@ -1011,14 +1055,15 @@ export class GroundUnit extends Unit { getIconOptions() { return { - showState: true, + showState: this.belongsToCommandedCoalition(), showVvi: false, - showHotgroup: true, - showUnitIcon: true, + showHotgroup: this.belongsToCommandedCoalition(), + showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), showShortLabel: false, showFuel: false, showAmmo: false, showSummary: false, + showCallsign: this.belongsToCommandedCoalition(), rotateToHeading: false }; } @@ -1035,14 +1080,15 @@ export class NavyUnit extends Unit { getIconOptions() { return { - showState: true, + showState: this.belongsToCommandedCoalition(), showVvi: false, showHotgroup: true, - showUnitIcon: true, - showShortLabel: true, + showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showShortLabel: false, showFuel: false, showAmmo: false, showSummary: false, + showCallsign: this.belongsToCommandedCoalition(), rotateToHeading: false }; } @@ -1063,11 +1109,12 @@ export class Weapon extends Unit { showState: false, showVvi: false, showHotgroup: false, - showUnitIcon: true, + showUnitIcon: this.belongsToCommandedCoalition(), showShortLabel: false, showFuel: false, showAmmo: false, showSummary: false, + showCallsign: false, rotateToHeading: true }; } diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 5d222291..8980cefe 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -1,13 +1,14 @@ import { LatLng, LatLngBounds } from "leaflet"; import { getHotgroupPanel, getInfoPopup, getMap, getMissionHandler } from ".."; import { Unit } from "./unit"; -import { cloneUnit, setLastUpdateTime, spawnGroundUnit } from "../server/server"; +import { cloneUnit, setLastUpdateTime, spawnGroundUnits } from "../server/server"; import { deg2rad, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polygonArea, randomPointInPoly, randomUnitBlueprintByRole } from "../other/utils"; import { CoalitionArea } from "../map/coalitionarea"; import { Airbase } from "../missionhandler/airbase"; import { groundUnitsDatabase } from "./groundunitsdatabase"; -import { DataIndexes, IADSRoles, IDLE, MOVE_UNIT } from "../constants/constants"; +import { DataIndexes, HIDE_ALL, IADSRoles, IDLE, MOVE_UNIT } from "../constants/constants"; import { DataExtractor } from "./dataextractor"; +import { Contact } from "../@types/unit"; export class UnitsManager { #units: { [ID: number]: Unit }; @@ -15,6 +16,7 @@ export class UnitsManager { #selectionEventDisabled: boolean = false; #pasteDisabled: boolean = false; #hiddenTypes: string[] = []; + #visibilityMode: string = HIDE_ALL; constructor() { this.#units = {}; @@ -27,6 +29,8 @@ export class UnitsManager { document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()); document.addEventListener('explodeSelectedUnits', () => this.selectedUnitsDelete(true)); document.addEventListener('keyup', (event) => this.#onKeyUp(event)); + document.addEventListener('exportToFile', () => this.exportToFile()); + document.addEventListener('importFromFile', () => this.importFromFile()); } getSelectableAircraft() { @@ -87,6 +91,12 @@ export class UnitsManager { this.#units[ID]?.setData(dataExtractor); } + for (let ID in this.#units) { + var unit = this.#units[ID]; + if (!unit.belongsToCommandedCoalition()) + unit.setDetectionMethods(this.getUnitDetectedMethods(unit)); + } + setLastUpdateTime(updateTime); } @@ -104,6 +114,24 @@ export class UnitsManager { return this.#hiddenTypes; } + setVisibilityMode(newVisibilityMode: string) { + if (newVisibilityMode !== this.#visibilityMode) { + document.dispatchEvent(new CustomEvent("visibilityModeChanged", { detail: this })); + const el = document.getElementById("visibiliy-mode"); + if (el) { + el.dataset.mode = newVisibilityMode; + el.textContent = newVisibilityMode.toUpperCase(); + } + this.#visibilityMode = newVisibilityMode; + for (let ID in this.#units) + this.#units[ID].updateVisibility(); + } + } + + getVisibilityMode() { + return this.#visibilityMode; + } + selectUnit(ID: number, deselectAllUnits: boolean = true) { if (deselectAllUnits) this.getSelectedUnits().filter((unit: Unit) => unit.ID !== ID).forEach((unit: Unit) => unit.setSelected(false)); @@ -479,6 +507,20 @@ export class UnitsManager { this.#showActionMessage(selectedUnits, `unit bombing point`); } + getUnitDetectedMethods(unit: Unit) { + var detectionMethods: number[] = []; + for (let idx in this.#units) { + if (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)) + detectionMethods.push(contact.detectionMethod); + }); + } + } + return detectionMethods; + } + /***********************************************/ copyUnits() { this.#copiedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ @@ -516,13 +558,49 @@ export class UnitsManager { const probability = Math.pow(1 - minDistance / 50e3, 5) * IADSRoles[role]; if (Math.random() < probability){ const unitBlueprint = randomUnitBlueprintByRole(groundUnitsDatabase, role); - const spawnOptions = {role: role, latlng: latlng, name: unitBlueprint.name, coalition: coalitionArea.getCoalition(), immediate: true}; - spawnGroundUnit(spawnOptions); - getMap().addTemporaryMarker(spawnOptions); + spawnGroundUnits([{unitName: unitBlueprint.name, latlng: latlng}], coalitionArea.getCoalition(), true); + getMap().addTemporaryMarker(latlng, unitBlueprint.name, coalitionArea.getCoalition()); } } } + exportToFile() { + var unitsToExport: {[key: string]: any} = {}; + for (let ID in this.#units) { + var unit = this.#units[ID]; + if (!["Aircraft", "Helicopter"].includes(unit.getCategory())) { + if (unit.getGroupName() in unitsToExport) + unitsToExport[unit.getGroupName()].push(unit.getData()); + else + unitsToExport[unit.getGroupName()] = [unit.getData()]; + } + } + var a = document.createElement("a"); + var file = new Blob([JSON.stringify(unitsToExport)], {type: 'text/plain'}); + a.href = URL.createObjectURL(file); + a.download = 'export.json'; + a.click(); + } + + importFromFile() { + var input = document.createElement("input"); + input.type = "file"; + input.addEventListener("change", (e: any) => { + var file = e.target.files[0]; + if (!file) { + return; + } + var reader = new FileReader(); + reader.onload = function(e: any) { + var contents = e.target.result; + var groups = JSON.parse(contents); + + }; + reader.readAsText(file); + }) + input.click(); + } + /***********************************************/ #onKeyUp(event: KeyboardEvent) { if (!keyEventWasInInput(event) && event.key === "Delete" ) { diff --git a/client/views/other/dialogs.ejs b/client/views/other/dialogs.ejs index bbf5badb..49e25a05 100644 --- a/client/views/other/dialogs.ejs +++ b/client/views/other/dialogs.ejs @@ -12,7 +12,7 @@ -

+

+ +
ArcGIS Satellite diff --git a/olympus.json b/olympus.json index 3618c236..683f600f 100644 --- a/olympus.json +++ b/olympus.json @@ -4,6 +4,8 @@ "port": 30000 }, "authentication": { - "password": "password" + "gameMasterPassword": "password", + "blueCommanderPassword": "bluepassword", + "redCommanderPassword": "redpassword" } } diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 7f086d6e..6ec47dae 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -303,105 +303,105 @@ function Olympus.explosion(intensity, lat, lng) trigger.action.explosion(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)), intensity) end --- Spawns a single ground unit -function Olympus.spawnGroundUnit(coalition, unitType, lat, lng) - Olympus.debug("Olympus.spawnGroundUnit " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..")", 2) - local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)) +-- Spawns a new unit or group +function Olympus.spawnUnits(spawnTable) + Olympus.debug("Olympus.spawnUnits " .. serializeTable(spawnTable), 2) local unitTable = {} + local route = {} + local category = nil - if Olympus.hasKey(templates, unitType) then - for idx, value in pairs(templates[unitType].units) do - unitTable[#unitTable + 1] = { - ["type"] = value.name, - ["x"] = spawnLocation.x + value.dx, - ["y"] = spawnLocation.z + value.dy, - ["playerCanDrive"] = true, - ["heading"] = 0, - ["skill"] = "High" - } - end - else - unitTable = - { - [1] = - { - ["type"] = unitType, - ["x"] = spawnLocation.x, - ["y"] = spawnLocation.z, - ["playerCanDrive"] = true, - ["heading"] = 0, - ["skill"] = "High" - }, - } + if spawnTable.category == 'Aircraft' then + unitTable = Olympus.generateAirUnitsTable(spawnTable.units) + route = Olympus.generateAirUnitsRoute(spawnTable) + category = 'airplane' + elseif spawnTable.category == 'GroundUnit' then + unitTable = Olympus.generateGroundUnitsTable(spawnTable.units) + category = 'vehicle' end - local countryID = Olympus.getCountryIDByCoalition(coalition) - + local countryID = Olympus.getCountryIDByCoalition(spawnTable.coalition) local vars = { units = unitTable, country = countryID, - category = 'vehicle', - name = "Ground-" .. Olympus.unitCounter, + category = category, + route = route, + name = "Olympus-" .. Olympus.unitCounter, } mist.dynAdd(vars) + Olympus.unitCounter = Olympus.unitCounter + 1 - Olympus.debug("Olympus.spawnGround completed succesfully", 2) -end + Olympus.debug("Olympus.spawnUnits completed succesfully", 2) +end --- Spawns a single aircraft. Spawn options are: --- payloadName: a string, one of the names defined in unitPayloads.lua. Must be compatible with the unitType --- airbaseName: a string, if present the aircraft will spawn on the ground of the selected airbase --- payload: a table, if present the unit will receive this specific payload. Overrides payloadName -function Olympus.spawnAircraft(coalition, unitType, lat, lng, alt, spawnOptions) - local payloadName = spawnOptions["payloadName"] - local airbaseName = spawnOptions["airbaseName"] - local payload = spawnOptions["payload"] - - Olympus.debug("Olympus.spawnAircraft " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..", " .. alt .. ")", 2) - local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)) - - if payload == nil then - if payloadName and payloadName ~= "" and Olympus.unitPayloads[unitType][payloadName] then - payload = Olympus.unitPayloads[unitType][payloadName] +-- Generates ground units table, either single or from template +function Olympus.generateGroundUnitsTable(units) + for idx, unit in pairs(units) do + local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0)) + local unitTable = {} + if Olympus.hasKey(templates, unit.unitType) then + for idx, value in pairs(templates[unit.unitType].units) do + unitTable[#unitTable + 1] = { + ["type"] = value.name, + ["x"] = spawnLocation.x + value.dx, + ["y"] = spawnLocation.z + value.dy, + ["playerCanDrive"] = true, + ["heading"] = 0, + ["skill"] = "High" + } + end else - payload = {} + unitTable[#unitTable + 1] = + { + ["type"] = unit.unitType, + ["x"] = unit.x, + ["y"] = unit.z, + ["playerCanDrive"] = true, + ["heading"] = 0, + ["skill"] = "High" + } end end - - local countryID = Olympus.getCountryIDByCoalition(coalition) - local unitTable = - { - [1] = + return unitTable +end + +-- Generates unit table for a air unit. +function Olympus.generateAirUnitsTable(units) + local unitTable = {} + for idx, unit in pairs(units) do + local payloadName = unit.payloadName -- payloadName: 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 payloadName + + if payload == nil then + if payloadName and payloadName ~= "" and Olympus.unitPayloads[unit.unitType][payloadName] then + payload = Olympus.unitPayloads[unit.unitType][payloadName] + else + payload = {} + end + end + + local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0)) + unitTable[#unitTable + 1] = { - ["type"] = unitType, + ["type"] = unit.unitType, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, - ["alt"] = alt, - ["alt_type"] = "BARO", + ["alt"] = unit.alt, + ["alt_type"] = "BARO", ["skill"] = "Excellent", - ["payload"] = - { - ["pylons"] = payload, - ["fuel"] = 999999, - ["flare"] = 60, - ["ammo_type"] = 1, - ["chaff"] = 60, - ["gun"] = 100, - }, + ["payload"] = { ["pylons"] = payload, ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100, }, ["heading"] = 0, - ["callsign"] = - { - [1] = 1, - [2] = 1, - [3] = 1, - ["name"] = "Olympus" .. Olympus.unitCounter, - }, - ["name"] = "Olympus-" .. Olympus.unitCounter - }, - } + ["callsign"] = { [1] = 1, [2] = 1, [3] = 1, ["name"] = "Olympus" .. Olympus.unitCounter.. "-" .. #unitTable + 1 }, + ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitTable + 1 + } + end + return unitTable + +function Olympus.generateAirUnitsRoute(spawnTable) + local airbaseName = spawnTable.airbaseName -- airbaseName: a string, if present the aircraft will spawn on the ground of the selected airbase + local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(spawnTable.lat, spawnTable.lng, 0)) -- If a airbase is provided the first waypoint is set as a From runway takeoff. local route = {} @@ -416,10 +416,9 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, alt, spawnOptions) [1] = { ["action"] = "From Parking Area Hot", - ["task"] = - { - ["id"] = "ComboTask", - ["params"] = {["tasks"] = {},}, + ["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"] = "TakeOffParkingHot", ["ETA"] = 0, @@ -442,69 +441,18 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, alt, spawnOptions) { ["alt"] = alt, ["alt_type"] = "BARO", - ["task"] = - { - ["id"] = "ComboTask", - ["params"] = - { - ["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" - }, - }, - }, - }, - }, + ["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"] = spawnLocation.x, ["y"] = spawnLocation.z, - }, -- end of [1] - }, -- end of ["points"] - } -- end of ["route"] + }, + }, + } end - - local vars = - { - units = unitTable, - country = countryID, - category = 'airplane', - name = "Olympus-" .. Olympus.unitCounter, - route = route, - task = 'CAP', - } - - local newGroup = mist.dynAdd(vars) - - -- Save the payload to be reused in case the unit is cloned. TODO: save by ID not by name (it works but I like consistency) - Olympus.payloadRegistry[vars.name] = payload - Olympus.unitCounter = Olympus.unitCounter + 1 - Olympus.debug("Olympus.spawnAir completed successfully", 2) + return route 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. @@ -513,15 +461,21 @@ function Olympus.clone(ID, lat, lng, category) local unit = Olympus.getUnitByID(ID) if unit then local coalition = Olympus.getCoalitionByCoalitionID(unit:getCoalition()) - - if category == "Aircraft" then - local spawnOptions = { - payload = Olympus.payloadRegistry[unit:getName()] + -- TODO: understand category in this script + local spawnTable = { + coalition = coalition, + category = category, + units = { + [1] = { + lat = lat, + lng = lng, + alt = unit:getPoint().y, + unitType = unit:getTypeName(), + payload = Olympus.payloadRegistry[unit:getName()] + } } - Olympus.spawnAircraft(coalition, unit:getTypeName(), lat, lng, unit:getPoint().y, spawnOptions) - elseif category == "GroundUnit" then - Olympus.spawnGroundUnit(coalition, unit:getTypeName(), lat, lng) - end + } + Olympus.spawnUnits(spawnTable) end Olympus.debug("Olympus.clone completed successfully", 2) end @@ -765,4 +719,4 @@ end timer.scheduleFunction(Olympus.setMissionData, {}, timer.getTime() + 1) -Olympus.notify("OlympusCommand script " .. version .. " loaded successfully", 2, true) \ No newline at end of file +Olympus.notify("OlympusCommand script " .. version .. " loaded successfully", 2, true) diff --git a/src/core/include/commands.h b/src/core/include/commands.h index 5c1769e0..91d42162 100644 --- a/src/core/include/commands.h +++ b/src/core/include/commands.h @@ -151,13 +151,13 @@ private: }; /* Spawn ground unit command */ -class SpawnGroundUnit : public Command +class SpawnGroundUnits : public Command { public: - SpawnGroundUnit(string coalition, string unitType, Coords location, bool immediate) : + SpawnGroundUnits(string coalition, vector unitTypes, vector locations, bool immediate) : coalition(coalition), - unitType(unitType), - location(location), + unitTypes(unitTypes), + locations(locations), immediate(immediate) { priority = immediate? CommandPriority::IMMEDIATE: CommandPriority::LOW; @@ -167,20 +167,20 @@ public: private: const string coalition; - const string unitType; - const Coords location; + const vector unitTypes; + const vector locations; const bool immediate; }; /* Spawn air unit command */ -class SpawnAircraft : public Command +class SpawnAircrafts : public Command { public: - SpawnAircraft(string coalition, string unitType, Coords location, string payloadName, string airbaseName, bool immediate) : + SpawnAircrafts(string coalition, vector unitTypes, vector locations, vector payloadNames, string airbaseName, bool immediate) : coalition(coalition), - unitType(unitType), - location(location), - payloadName(payloadName), + unitTypes(unitTypes), + locations(locations), + payloadNames(payloadNames), airbaseName(airbaseName), immediate(immediate) { @@ -191,9 +191,9 @@ public: private: const string coalition; - const string unitType; - const Coords location; - const string payloadName; + const vector unitTypes; + const vector locations; + const vector payloadNames; const string airbaseName; const bool immediate; }; diff --git a/src/core/include/server.h b/src/core/include/server.h index 6880f7b4..2bcddddd 100644 --- a/src/core/include/server.h +++ b/src/core/include/server.h @@ -24,10 +24,16 @@ private: void handle_request(http_request request, function action); void handle_put(http_request request); + string extractPassword(http_request& request); + void task(); atomic runListener; - string password = ""; + string gameMasterPassword = ""; + string blueCommanderPassword = ""; + string redCommanderPassword = ""; + string atcPassword = ""; + string observerPassword = ""; }; diff --git a/src/core/src/commands.cpp b/src/core/src/commands.cpp index fbd6da60..450158a0 100644 --- a/src/core/src/commands.cpp +++ b/src/core/src/commands.cpp @@ -37,38 +37,52 @@ string Smoke::getString(lua_State* L) return commandSS.str(); } -/* Spawn ground command */ -string SpawnGroundUnit::getString(lua_State* L) +/* Spawn ground units command */ +string SpawnGroundUnits::getString(lua_State* L) { + if (unitTypes.size() != locations.size()) return ""; + + std::ostringstream unitsSS; + unitsSS.precision(10); + for (int i = 0; i < unitTypes.size(); i++) { + unitsSS << "[" << i + 1 << "] = {" + << "unitType = " << "\"" << unitTypes[i] << "\"" << ", " + << "lat = " << locations[i].lat << ", " + << "lng = " << locations[i].lng << "}"; + } + std::ostringstream commandSS; commandSS.precision(10); - commandSS << "Olympus.spawnGroundUnit, " - << "\"" << coalition << "\"" << ", " - << "\"" << unitType << "\"" << ", " - << location.lat << ", " - << location.lng; + commandSS << "Olympus.spawnUnits, {" + << "category = " << "\"" << "GroundUnit" << "\"" << ", " + << "coalition = " << "\"" << coalition << "\"" << ", " + << "units = " << "\"" << unitsSS.str() << "\"" << "}"; return commandSS.str(); } -/* Spawn air command */ -string SpawnAircraft::getString(lua_State* L) +/* Spawn aircrafts command */ +string SpawnAircrafts::getString(lua_State* L) { - std::ostringstream optionsSS; - optionsSS.precision(10); - optionsSS << "{" - << "payloadName = \"" << payloadName << "\", " - << "airbaseName = \"" << airbaseName << "\", " - << "}"; + if (unitTypes.size() != locations.size() || unitTypes.size() != payloadNames.size()) return ""; + + std::ostringstream unitsSS; + unitsSS.precision(10); + for (int i = 0; i < unitTypes.size(); i++) { + unitsSS << "[" << i + 1 << "] = {" + << "unitType = " << "\"" << unitTypes[i] << "\"" << ", " + << "lat = " << locations[i].lat << ", " + << "lng = " << locations[i].lng << ", " + << "alt = " << locations[i].alt << ", " + << "payloadName = \"" << payloadNames[i] << "\", " << "}"; + } std::ostringstream commandSS; commandSS.precision(10); - commandSS << "Olympus.spawnAircraft, " - << "\"" << coalition << "\"" << ", " - << "\"" << unitType << "\"" << ", " - << location.lat << ", " - << location.lng << ", " - << location.alt << ", " - << optionsSS.str(); + commandSS << "Olympus.spawnUnits, {" + << "category = " << "\"" << "Aircraft" << "\"" << ", " + << "coalition = " << "\"" << coalition << "\"" << ", " + << "airbaseName = \"" << airbaseName << "\", " + << "units = " << "\"" << unitsSS.str() << "\"" << "}"; return commandSS.str(); } diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp index 2346c1ea..799faea0 100644 --- a/src/core/src/scheduler.cpp +++ b/src/core/src/scheduler.cpp @@ -92,30 +92,49 @@ void Scheduler::handleRequest(string key, json::value value) Coords loc; loc.lat = lat; loc.lng = lng; command = dynamic_cast(new Smoke(color, loc)); } - else if (key.compare("spawnGround") == 0) + else if (key.compare("spawnGroundUnits") == 0) { bool immediate = value[L"immediate"].as_bool(); string coalition = to_string(value[L"coalition"]); - string type = to_string(value[L"type"]); - double lat = value[L"location"][L"lat"].as_double(); - double lng = value[L"location"][L"lng"].as_double(); - log("Spawning " + coalition + " ground unit of type " + type + " at (" + to_string(lat) + ", " + to_string(lng) + ")"); - Coords loc; loc.lat = lat; loc.lng = lng; - command = dynamic_cast(new SpawnGroundUnit(coalition, type, loc, immediate)); + + vector unitTypes; + vector locations; + for (auto unit : value[L"units"].as_array()) { + string unitType = to_string(unit[L"type"]); + 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; + log("Spawning " + coalition + " ground unit of type " + unitType + " at (" + to_string(lat) + ", " + to_string(lng) + ")"); + unitTypes.push_back(unitType); + locations.push_back(location); + } + + command = dynamic_cast(new SpawnGroundUnits(coalition, unitTypes, locations, immediate)); } - else if (key.compare("spawnAir") == 0) + else if (key.compare("spawnAircrafts") == 0) { bool immediate = value[L"immediate"].as_bool(); string coalition = to_string(value[L"coalition"]); - string type = to_string(value[L"type"]); - double lat = value[L"location"][L"lat"].as_double(); - double lng = value[L"location"][L"lng"].as_double(); - double altitude = value[L"altitude"].as_double(); - Coords loc; loc.lat = lat; loc.lng = lng; loc.alt = altitude; - string payloadName = to_string(value[L"payloadName"]); string airbaseName = to_string(value[L"airbaseName"]); - log("Spawning " + coalition + " air unit of type " + type + " with payload " + payloadName + " at (" + to_string(lat) + ", " + to_string(lng) + " " + airbaseName + ")"); - command = dynamic_cast(new SpawnAircraft(coalition, type, loc, payloadName, airbaseName, immediate)); + + vector unitTypes; + vector locations; + vector payloadNames; + for (auto unit : value[L"units"].as_array()) { + string unitType = to_string(unit[L"type"]); + double lat = unit[L"location"][L"lat"].as_double(); + double lng = unit[L"location"][L"lng"].as_double(); + double alt = value[L"altitude"].as_double(); + Coords location; location.lat = lat; location.lng = lng; location.alt = alt; + string payloadName = to_string(value[L"payloadName"]); + + log("Spawning " + coalition + " air unit unit of type " + unitType + " at (" + to_string(lat) + ", " + to_string(lng) + ")"); + unitTypes.push_back(unitType); + locations.push_back(location); + payloadNames.push_back(payloadName); + } + + command = dynamic_cast(new SpawnAircrafts(coalition, unitTypes, locations, payloadNames, airbaseName, immediate)); } else if (key.compare("attackUnit") == 0) { diff --git a/src/core/src/server.cpp b/src/core/src/server.cpp index 580b24d7..7a27bcec 100644 --- a/src/core/src/server.cpp +++ b/src/core/src/server.cpp @@ -72,8 +72,9 @@ void Server::handle_get(http_request request) milliseconds ms = duration_cast(system_clock::now().time_since_epoch()); http_response response(status_codes::OK); - string authorization = to_base64("admin:" + password); - if (password.length() == 0 || (request.headers().has(L"Authorization") && request.headers().find(L"Authorization")->second.compare(L"Basic " + to_wstring(authorization)) == 0)) + + string password = extractPassword(request); + if (password.compare(gameMasterPassword) == 0 || password.compare(blueCommanderPassword) == 0 || password.compare(redCommanderPassword) == 0) { std::exception_ptr eptr; try { @@ -114,9 +115,15 @@ void Server::handle_get(http_request request) answer[L"airbases"] = airbases; else if (URI.compare(BULLSEYE_URI) == 0) answer[L"bullseyes"] = bullseyes; - else if (URI.compare(MISSION_URI) == 0) + else if (URI.compare(MISSION_URI) == 0) { + if (password.compare(gameMasterPassword) == 0) + mission[L"visibilityMode"] = json::value(L"Game master"); + else if (password.compare(blueCommanderPassword) == 0) + mission[L"visibilityMode"] = json::value(L"Blue commander"); + else if (password.compare(redCommanderPassword) == 0) + mission[L"visibilityMode"] = json::value(L"Red commander"); answer[L"mission"] = mission; - + } answer[L"time"] = json::value::string(to_wstring(ms.count())); answer[L"sessionHash"] = json::value::string(to_wstring(sessionHash)); @@ -144,8 +151,10 @@ void Server::handle_get(http_request request) void Server::handle_request(http_request request, function action) { http_response response(status_codes::OK); - string authorization = to_base64("admin:" + password); - if (password.length() == 0 || (request.headers().has(L"Authorization") && request.headers().find(L"Authorization")->second.compare(L"Basic " + to_wstring(authorization)) == 0)) + + //TODO: limit what a user can do depending on the password + string password = extractPassword(request); + if (password.compare(gameMasterPassword) == 0 || password.compare(blueCommanderPassword) == 0 || password.compare(redCommanderPassword) == 0) { auto answer = json::value::object(); request.extract_json().then([&answer, &action](pplx::task task) @@ -200,6 +209,30 @@ void Server::handle_put(http_request request) }); } +string Server::extractPassword(http_request& request) { + if (request.headers().has(L"Authorization")) { + string authorization = to_string(request.headers().find(L"Authorization")->second); + string s = "Basic "; + string::size_type i = authorization.find(s); + + if (i != std::string::npos) + authorization.erase(i, s.length()); + else + return ""; + + string decoded = from_base64(authorization); + i = authorization.find(":"); + if (i != std::string::npos) + decoded.erase(0, i); + else + return ""; + + return decoded; + } + else + return ""; +} + void Server::task() { string address = REST_ADDRESS; @@ -225,7 +258,9 @@ void Server::task() if (config.is_object() && config.has_object_field(L"authentication") && config[L"authentication"].has_string_field(L"password")) { - password = to_string(config[L"authentication"][L"password"]); + gameMasterPassword = to_string(config[L"authentication"][L"gameMasterPassword"]); + blueCommanderPassword = to_string(config[L"authentication"][L"blueCommanderPassword"]); + redCommanderPassword = to_string(config[L"authentication"][L"redCommanderPassword"]); } else log("Error reading configuration file. No password set.");