├── README.md ├── client.lua ├── config.lua ├── fxmanifest.lua ├── http ├── blip.svg ├── blip_dead.svg ├── blip_pin.svg ├── gta5 │ ├── map.jpg │ └── pricedown.otf ├── index.html ├── rdr3 │ ├── chineserocks.ttf │ └── map.jpg ├── script.js └── style.css └── server.lua /README.md: -------------------------------------------------------------------------------- 1 | # FiveM/RedM Web Map 2 | 3 | Live web-based map showing the locations of players and other info about the server. 4 | 5 | # Examples 6 | 7 | - https://fivem.khzae.net/map/ 8 | - https://redm.khzae.net/map/ 9 | 10 | # Requirements 11 | 12 | - [httpmanager](https://github.com/kibook/httpmanager) 13 | - [weathersync](https://github.com/kibook/redm-weathersync) 14 | 15 | # Installation 16 | 17 | 1. Copy to a directory in the `resources` folder of your server. 18 | 19 | Example: `resources/[local]/webmap` 20 | 21 | 2. Add `start webmap` to `server.cfg`. 22 | 23 | 3. Access the map at `http://:/webmap/` or `https://-.users.cfx.re/webmap/` (**Note:** The trailing slash is necessary). 24 | 25 | Examples: 26 | - http://redm.khzae.net:30120/webmap/ 27 | - https://kibukj-jqv8ok.users.cfx.re/webmap/ 28 | - http://fivem.khzae.net:30120/webmap/ 29 | - https://kibukj-8l4kjb.users.cfx.re/webmap/ 30 | -------------------------------------------------------------------------------- /client.lua: -------------------------------------------------------------------------------- 1 | RegisterNetEvent("webmap:updateInfo") 2 | 3 | AddEventHandler("webmap:updateInfo", function() 4 | local playerPed = PlayerPedId() 5 | 6 | TriggerServerEvent("webmap:updateInfo", { 7 | name = GetPlayerName(PlayerId()), 8 | coords = GetEntityCoords(playerPed), 9 | heading = GetEntityHeading(playerPed), 10 | health = GetEntityHealth(playerPed) 11 | }) 12 | end) 13 | -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | Config = {} 2 | 3 | -- How often the server queries information from clients 4 | Config.updateInterval = 5000 5 | 6 | -- Enable or disable weather info on the live map (requires weathersync if enabled) 7 | Config.displayWeather = true 8 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version "cerulean" 2 | games {"gta5", "rdr3"} 3 | rdr3_warning "I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships." 4 | 5 | name "webmap" 6 | author "kibukj" 7 | description "Live map for FiveM and RedM servers" 8 | url "https://github.com/kibook/webmap" 9 | 10 | dependency "httpmanager" -- https://github.com/kibook/httpmanager 11 | 12 | -- You can comment this out if you turn off Config.displayWeather 13 | dependency "weathersync" -- https://github.com/kibook/weathersync 14 | 15 | server_scripts { 16 | "config.lua", 17 | "server.lua" 18 | } 19 | 20 | client_script "client.lua" 21 | -------------------------------------------------------------------------------- /http/blip.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 36 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /http/blip_dead.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 38 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /http/blip_pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /http/gta5/map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kibook/webmap/18caff0d64fdfca6196c355d0a5d18d19d303aed/http/gta5/map.jpg -------------------------------------------------------------------------------- /http/gta5/pricedown.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kibook/webmap/18caff0d64fdfca6196c355d0a5d18d19d303aed/http/gta5/pricedown.otf -------------------------------------------------------------------------------- /http/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Live Map 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 | 29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /http/rdr3/chineserocks.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kibook/webmap/18caff0d64fdfca6196c355d0a5d18d19d303aed/http/rdr3/chineserocks.ttf -------------------------------------------------------------------------------- /http/rdr3/map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kibook/webmap/18caff0d64fdfca6196c355d0a5d18d19d303aed/http/rdr3/map.jpg -------------------------------------------------------------------------------- /http/script.js: -------------------------------------------------------------------------------- 1 | /* Map settings for RedM */ 2 | const rdr3Map = { 3 | width: 11820, 4 | height: 8660, 5 | xOffset: 8515, 6 | yOffset: 10850 7 | }; 8 | 9 | /* Map settings for FiveM */ 10 | const gta5Map = { 11 | width: 12340, 12 | height: 12200, 13 | xOffset: 10390, 14 | yOffset: 12150 15 | }; 16 | 17 | /* Radius of the entire in-game world. */ 18 | const mapRadius = 16000; 19 | 20 | /* The number of in-game distance units wide the map graphic is. */ 21 | let mapWidth = 0; 22 | 23 | /* The number of in-game distance units tall the map graphic is. */ 24 | let mapHeight = 0; 25 | 26 | /* The number of in-game distance units the map graphic is offset from the minimum X coordinate. */ 27 | let mapXOffset = 0; 28 | 29 | /* The number of in-game distance units the map graphic is offset from the minimum Y coordinate. */ 30 | let mapYOffset = 0; 31 | 32 | /* How often to fetch updates to the map. */ 33 | const updateInterval = 5000; 34 | 35 | /* URL to get configuration from. */ 36 | const configUrl = 'config.json'; 37 | 38 | /* URL to fetch server info from. */ 39 | const updateUrl = 'info.json'; 40 | 41 | /* Whether weathersync info is enabled. */ 42 | let displayWeather = false; 43 | 44 | /* Icons for each type of weather. */ 45 | const gta5WeatherIcons = { 46 | blizzard: "❄️", 47 | clear: "☀️", 48 | clearing: "🌦️", 49 | clouds: "⛅", 50 | extrasunny: "☀️", 51 | foggy: "🌫️", 52 | halloween: "🎃", 53 | neutral: "🌧️", 54 | overcast: "☁️", 55 | rain: "🌧️", 56 | smog: "🌫️", 57 | snow: "🌨️", 58 | snowlight: "🌨️", 59 | thunder: "⛈️", 60 | xmas: "🌨️" 61 | }; 62 | 63 | const rdr3WeatherIcons = { 64 | blizzard: "❄️", 65 | clouds: "⛅", 66 | drizzle: "🌦️", 67 | fog: "🌫️", 68 | groundblizzard: "❄️", 69 | hail: "🌨️", 70 | highpressure: "☀️", 71 | hurricane: "🌀", 72 | misty: "🌫️", 73 | overcast: "☁️", 74 | overcastdark: "☁️", 75 | rain: "🌧️", 76 | sandstorm: "🌪️", 77 | shower: "🌧️", 78 | sleet: "🌧️", 79 | snow: "🌨️", 80 | snowlight: "🌨️", 81 | sunny: "☀️", 82 | thunder: "🌩️", 83 | thunderstorm: "⛈️", 84 | whiteout: "❄️" 85 | }; 86 | 87 | let weatherIcons = {}; 88 | 89 | let customPoints = []; 90 | 91 | function dayOfWeek(day) { 92 | return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][day]; 93 | } 94 | 95 | function cardinalDirection(h) { 96 | if (h <= 22.5) { 97 | return "N"; 98 | } else if (h <= 67.5) { 99 | return "NE"; 100 | } else if (h <= 112.5) { 101 | return "E"; 102 | } else if (h <= 157.5) { 103 | return "SE"; 104 | } else if (h <= 202.5) { 105 | return "S"; 106 | } else if (h <= 247.5) { 107 | return "SW"; 108 | } else if (h <= 292.5) { 109 | return "W"; 110 | } else if (h <= 337.5) { 111 | return "NW"; 112 | } else { 113 | return "N"; 114 | } 115 | } 116 | 117 | function timeToString(time) { 118 | return `${String(time.hour).padStart(2, "0")}:${String(time.minute).padStart(2, "0")}:${String(time.second).padStart(2, "0")}`; 119 | } 120 | 121 | function dayAndTimeToString(time) { 122 | return `${dayOfWeek(time.day)} ${timeToString(time)}` 123 | } 124 | 125 | function mapOnMouseMove(event) { 126 | var width = event.clientX / this.offsetWidth; 127 | var height = Math.abs(event.clientY - this.offsetHeight) / this.offsetHeight; 128 | 129 | var x = width * mapWidth + mapXOffset - mapRadius 130 | var y = height * mapHeight + mapYOffset - mapRadius 131 | 132 | document.getElementById("coords").innerHTML = `${x.toFixed(3)}, ${y.toFixed(3)}` 133 | } 134 | 135 | function tabButtonOnClick(event) { 136 | document.querySelectorAll(".tab").forEach(tab => tab.style.display = "none"); 137 | document.querySelectorAll(".tab-button").forEach(button => button.className = "tab-button"); 138 | document.getElementById(this.getAttribute("data-tab")).style.display = "block"; 139 | this.className = "tab-button active"; 140 | } 141 | 142 | function addBlip(x, y, z, heading, blipClass, tag) { 143 | let blip = document.createElement('div'); 144 | 145 | blip.className = blipClass; 146 | 147 | let left = (x + mapRadius - mapXOffset) / mapWidth * 100; 148 | let bottom = (y + mapRadius - mapYOffset) / mapHeight * 100; 149 | 150 | if (left < 0) { 151 | left = 0; 152 | } else if (left > 100) { 153 | left = 100; 154 | } 155 | 156 | if (bottom < 0) { 157 | bottom = 0; 158 | } else if (bottom > 100) { 159 | bottom = 100; 160 | } 161 | 162 | blip.style.left = `${left}%`; 163 | blip.style.bottom = `${bottom}%`; 164 | 165 | blip.style.transform = `rotate(-${heading}deg)`; 166 | 167 | let blipTag = document.createElement('div'); 168 | blipTag.className = 'blip-tag'; 169 | blipTag.style.left = `${left}%` 170 | blipTag.style.bottom = `${bottom}%` 171 | 172 | if (typeof tag == 'string') { 173 | blipTag.innerHTML = tag; 174 | } else { 175 | tag.forEach(e => blipTag.appendChild(e)); 176 | } 177 | 178 | document.getElementById('blips').appendChild(blip); 179 | document.getElementById('blip-tags').appendChild(blipTag); 180 | } 181 | 182 | function updateMap() { 183 | fetch(updateUrl).then(resp => resp.json()).then(info => { 184 | document.title = `${info.serverName} Live Map`; 185 | 186 | var serverName = document.getElementById('server-name'); 187 | 188 | serverName.innerHTML = info.serverName; 189 | 190 | var time = document.getElementById('time'); 191 | var weather = document.getElementById('weather'); 192 | var wind = document.getElementById("wind"); 193 | 194 | if (displayWeather) { 195 | time.innerHTML = dayAndTimeToString(info.time); 196 | weather.innerHTML = weatherIcons[info.weather]; 197 | weather.title = info.weather; 198 | wind.innerHTML = cardinalDirection(info.wind.direction); 199 | } 200 | 201 | var playerList = document.getElementById('player-list'); 202 | var blips = document.getElementById('blips'); 203 | var blipTags = document.getElementById('blip-tags'); 204 | 205 | playerList.innerHTML = ''; 206 | blips.innerHTML = ''; 207 | blipTags.innerHTML = ''; 208 | 209 | Object.keys(info.players).forEach(player => { 210 | var playerInfo = info.players[player]; 211 | 212 | if (playerInfo) { 213 | var playerDiv = document.createElement('div'); 214 | playerDiv.className = 'player'; 215 | 216 | var playerIdDiv = document.createElement('div'); 217 | playerIdDiv.className = 'player-id'; 218 | playerIdDiv.innerHTML = player; 219 | 220 | var playerNameDiv = document.createElement('div'); 221 | playerNameDiv.className = 'player-name'; 222 | playerNameDiv.innerHTML = playerInfo.name; 223 | 224 | var playerHealthDiv = document.createElement('div'); 225 | playerHealthDiv.className = 'player-health'; 226 | 227 | var playerHealthIconDiv = document.createElement('div'); 228 | playerHealthIconDiv.className = 'player-health-icon'; 229 | playerHealthIconDiv.innerHTML = ''; 230 | 231 | var playerHealthValueDiv = document.createElement('div'); 232 | playerHealthValueDiv.className = 'player-health-value'; 233 | playerHealthValueDiv.innerHTML = playerInfo.health; 234 | 235 | playerHealthDiv.appendChild(playerHealthIconDiv); 236 | playerHealthDiv.appendChild(playerHealthValueDiv); 237 | 238 | playerDiv.appendChild(playerIdDiv); 239 | playerDiv.appendChild(playerNameDiv); 240 | playerDiv.appendChild(playerHealthDiv); 241 | playerList.appendChild(playerDiv); 242 | 243 | let blipClass = playerInfo.health == 0 ? 'blip dead' : 'blip'; 244 | let heading = playerInfo.health == 0 ? 0 : playerInfo.heading; 245 | 246 | var blipTagPlayerName = document.createElement('div'); 247 | blipTagPlayerName.className = 'player-name'; 248 | blipTagPlayerName.innerHTML = playerInfo.name; 249 | 250 | var blipTagPlayerHealth = document.createElement('div'); 251 | blipTagPlayerHealth.className = 'player-health'; 252 | 253 | var blipTagPlayerHealthIcon = document.createElement('div'); 254 | blipTagPlayerHealthIcon.className = 'player-health-icon'; 255 | blipTagPlayerHealthIcon.innerHTML = ''; 256 | 257 | var blipTagPlayerHealthValue = document.createElement('div'); 258 | blipTagPlayerHealthValue.className = 'player-health-value'; 259 | blipTagPlayerHealthValue.innerHTML = playerInfo.health; 260 | 261 | blipTagPlayerHealth.appendChild(blipTagPlayerHealthIcon); 262 | blipTagPlayerHealth.appendChild(blipTagPlayerHealthValue); 263 | 264 | addBlip(playerInfo.coords.x, playerInfo.coords.y, playerInfo.coords.z, heading, blipClass, [blipTagPlayerName, blipTagPlayerHealth]); 265 | } 266 | }); 267 | 268 | customPoints.forEach(point => { 269 | addBlip(point.x, point.y, point.z, 0, 'blip pin', point.name); 270 | }); 271 | 272 | if (displayWeather) { 273 | var forecastDiv = document.getElementById("forecast"); 274 | 275 | forecastDiv.innerHTML = ""; 276 | 277 | var prevDay; 278 | 279 | info.forecast.forEach(entry => { 280 | var dayDiv = document.createElement("div"); 281 | dayDiv.className = "forecast-day"; 282 | if (entry.day != prevDay) { 283 | dayDiv.innerHTML = dayOfWeek(entry.day); 284 | prevDay = entry.day; 285 | } 286 | 287 | var timeDiv = document.createElement("div"); 288 | timeDiv.className = "forecast-time"; 289 | timeDiv.innerHTML = timeToString(entry); 290 | 291 | var weatherDiv = document.createElement("div"); 292 | weatherDiv.className = "forecast-weather"; 293 | weatherDiv.innerHTML = weatherIcons[entry.weather]; 294 | weatherDiv.title = entry.weather; 295 | 296 | var windDiv = document.createElement("div"); 297 | windDiv.className = "forecast-wind"; 298 | windDiv.innerHTML = cardinalDirection(entry.wind); 299 | 300 | forecastDiv.appendChild(dayDiv); 301 | forecastDiv.appendChild(timeDiv); 302 | forecastDiv.appendChild(weatherDiv); 303 | forecastDiv.appendChild(windDiv); 304 | }); 305 | } 306 | }); 307 | } 308 | 309 | function addCustomPoint(name, x, y, z) { 310 | customPoints.push({name: name, x: x, y: y, z: z}); 311 | } 312 | 313 | window.addEventListener("load", event => { 314 | let map = document.getElementById('map'); 315 | 316 | map.addEventListener("mousemove", mapOnMouseMove); 317 | 318 | document.querySelectorAll("#tab-bar button").forEach(button => button.addEventListener("click", tabButtonOnClick)); 319 | 320 | let url = new URL(window.location); 321 | let x = url.searchParams.get("x"); 322 | let y = url.searchParams.get("y"); 323 | let z = url.searchParams.get("z"); 324 | 325 | if (x && y && z) { 326 | addCustomPoint(`${x}, ${y}, ${z}`, parseFloat(x), parseFloat(y), parseFloat(z)); 327 | } 328 | 329 | fetch(configUrl).then(resp => resp.json()).then(resp => { 330 | if (resp.gameName == "gta5") { 331 | document.querySelectorAll('.tab-button').forEach(e => e.style.fontFamily = "Pricedown"); 332 | document.body.style.backgroundColor = '#0fa8d2'; 333 | map.style.backgroundImage = 'url("gta5/map.jpg")'; 334 | 335 | mapWidth = gta5Map.width; 336 | mapHeight = gta5Map.height; 337 | mapXOffset = gta5Map.xOffset; 338 | mapYOffset = gta5Map.yOffset; 339 | 340 | weatherIcons = gta5WeatherIcons; 341 | } else { 342 | document.querySelectorAll('.tab-button').forEach(e => e.style.fontFamily = "Chinese Rocks"); 343 | document.body.style.backgroundColor = '#d4b891'; 344 | map.style.backgroundImage = 'url("rdr3/map.jpg")'; 345 | 346 | mapWidth = rdr3Map.width; 347 | mapHeight = rdr3Map.height; 348 | mapXOffset = rdr3Map.xOffset; 349 | mapYOffset = rdr3Map.yOffset; 350 | 351 | weatherIcons = rdr3WeatherIcons; 352 | } 353 | 354 | displayWeather = resp.displayWeather; 355 | 356 | if (!displayWeather) { 357 | document.getElementById('forecast-button').remove(); 358 | document.getElementById('weather-container').remove(); 359 | } 360 | 361 | updateMap(); 362 | setInterval(updateMap, updateInterval); 363 | }); 364 | }); 365 | -------------------------------------------------------------------------------- /http/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Chinese Rocks"; 3 | src: url("rdr3/chineserocks.ttf"); 4 | } 5 | 6 | @font-face { 7 | font-family: "Pricedown"; 8 | src: url("gta5/pricedown.otf"); 9 | } 10 | 11 | html, body { 12 | width: 100vw; 13 | height: 100vh; 14 | padding: 0; 15 | margin: 0; 16 | font-family: Helvetica; 17 | } 18 | 19 | body { 20 | background-color: #000; 21 | } 22 | 23 | a { 24 | color: cornflowerblue; 25 | text-decoration: none; 26 | } 27 | 28 | #map { 29 | position: absolute; 30 | width: 100%; 31 | height: 100%; 32 | background-size: 100% 100%; 33 | overflow: hidden; 34 | } 35 | 36 | #server-name { 37 | position: absolute; 38 | bottom: 3%; 39 | left: 50%; 40 | transform: translate(-50%, 0); 41 | color: white; 42 | background-color: rgba(0, 0, 0, 0.5); 43 | padding: 0.5vh 0.5vw; 44 | font-size: 2.5vh; 45 | } 46 | 47 | #info { 48 | display: flex; 49 | flex-flow: column; 50 | position: absolute; 51 | top: 3%; 52 | left: 3%; 53 | background-color: rgba(0, 0, 0, 0.5); 54 | padding: 1vh 1vw; 55 | color: white; 56 | } 57 | 58 | #weather-container { 59 | font-size: 1.7vh; 60 | } 61 | 62 | #time, #weather, #wind { 63 | display: inline-block; 64 | } 65 | 66 | #wind { 67 | font-size: 75%; 68 | } 69 | 70 | #tab-bar { 71 | padding-top: 1vh; 72 | display: flex; 73 | gap: 0.5vh 0.5vw; 74 | } 75 | 76 | .tab-button { 77 | background: none; 78 | border: none; 79 | color: rgba(255, 255, 255, 0.5); 80 | font-size: 2.5vh; 81 | padding: 0; 82 | border-bottom: solid 1px rgba(0, 0, 0, 0); 83 | } 84 | 85 | .tab-button:active, .tab-button:hover, .tab-button:focus { 86 | border-bottom: 1px solid white; 87 | } 88 | 89 | .tab-button.active { 90 | color: white; 91 | border-bottom: 1px solid red; 92 | } 93 | 94 | .tab { 95 | padding-top: 0.5vh; 96 | } 97 | 98 | #player-list { 99 | display: grid; 100 | grid-template-columns: repeat(3, max-content); 101 | grid-gap: 1vh 1vw; 102 | font-size: 1.5vh; 103 | padding-top: 1vh; 104 | } 105 | 106 | .player { 107 | display: contents; 108 | } 109 | 110 | .player-id { 111 | text-align: right; 112 | } 113 | 114 | .player-name, .player-health { 115 | white-space: nowrap; 116 | } 117 | 118 | .player-health { 119 | display: grid; 120 | grid-template-columns: repeat(2, max-content); 121 | grid-gap: 0.25em; 122 | align-items: baseline; 123 | text-align: right; 124 | } 125 | 126 | .blip { 127 | background-image: url("blip.svg"); 128 | background-size: contain; 129 | background-repeat: no-repeat; 130 | background-position: center; 131 | position: absolute; 132 | width: 1vw; 133 | height: 1vw; 134 | } 135 | 136 | .blip.dead { 137 | background-image: url("blip_dead.svg"); 138 | } 139 | 140 | .blip.pin { 141 | background-image: url("blip_pin.svg"); 142 | width: 1.5vw; 143 | height: 1.5vw; 144 | } 145 | 146 | .blip-tag { 147 | position: absolute; 148 | transform: translate(-100%, -100%); 149 | color: white; 150 | background-color: rgba(0, 0, 0, 0.5); 151 | font-size: 1.5vh; 152 | padding: 0.25vh 0.25vw; 153 | display: grid; 154 | grid-template-columns: auto auto; 155 | grid-gap: 0.5vh 0.5vw; 156 | } 157 | 158 | #forecast { 159 | display: grid; 160 | grid-template-columns: repeat(4, max-content); 161 | grid-gap: 0.25vh 0.5vw; 162 | align-items: baseline; 163 | font-size: 1.7vh; 164 | text-align: right; 165 | } 166 | 167 | .forecast-wind { 168 | font-size: 75%; 169 | text-align: left; 170 | } 171 | 172 | #coords { 173 | position: absolute; 174 | right: 3%; 175 | bottom: 3%; 176 | font-size: 1.25vh; 177 | } 178 | -------------------------------------------------------------------------------- /server.lua: -------------------------------------------------------------------------------- 1 | local players = {} 2 | 3 | RegisterNetEvent("webmap:updateInfo") 4 | 5 | local function prunePlayers() 6 | for player, info in pairs(players) do 7 | if not GetPlayerEndpoint(player) then 8 | players[player] = nil 9 | end 10 | end 11 | end 12 | 13 | AddEventHandler("playerDropped", function(reason) 14 | players[source] = nil 15 | end) 16 | 17 | AddEventHandler("webmap:updateInfo", function(playerInfo) 18 | players[source] = playerInfo 19 | end) 20 | 21 | Citizen.CreateThread(function() 22 | print("Access the live map at: " .. exports.httpmanager:getUrl()) 23 | 24 | while true do 25 | prunePlayers() 26 | TriggerClientEvent("webmap:updateInfo", -1) 27 | Citizen.Wait(Config.updateInterval) 28 | end 29 | end) 30 | 31 | SetHttpHandler(exports.httpmanager:createHttpHandler { 32 | routes = { 33 | ["^/info.json$"] = function(req, res, helpers) 34 | local data = {} 35 | 36 | data.serverName = GetConvar("sv_projectName", GetConvar("sv_hostname", "Server Name")) 37 | data.players = players 38 | 39 | if Config.displayWeather then 40 | data.time = exports.weathersync:getTime() 41 | data.weather = exports.weathersync:getWeather() 42 | data.wind = exports.weathersync:getWind() 43 | data.forecast = exports.weathersync:getForecast() 44 | end 45 | 46 | res.sendJson(data) 47 | end, 48 | ["^/config.json$"] = function(req, res, helpers) 49 | res.sendJson { 50 | gameName = GetConvar("gamename", "gta5"), 51 | displayWeather = Config.displayWeather 52 | } 53 | end 54 | } 55 | }) 56 | --------------------------------------------------------------------------------