├── [maps] └── .gitkeep ├── .gitignore ├── objectloader ├── fxmanifest.lua ├── config.lua ├── slaxml.lua └── client.lua ├── [examples] ├── example1 │ ├── fxmanifest.lua │ └── butterbridge.xml └── example2 │ ├── fxmanifest.lua │ ├── map2.xml │ └── map1.xml └── README.md /[maps]/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /\[maps\]/ 2 | -------------------------------------------------------------------------------- /objectloader/fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'adamant' 2 | game '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 | client_scripts { 6 | 'config.lua', 7 | 'slaxml.lua', 8 | 'client.lua' 9 | } 10 | -------------------------------------------------------------------------------- /[examples]/example1/fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'adamant' 2 | game '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 | dependency 'objectloader' 6 | file 'butterbridge.xml' 7 | objectloader_map 'butterbridge.xml' 8 | -------------------------------------------------------------------------------- /objectloader/config.lua: -------------------------------------------------------------------------------- 1 | Config = {} 2 | 3 | -- Distance at which entities spawn/despawn 4 | Config.SpawnDistance = 200 5 | 6 | -- Timeout before attempting to restart a crashed map thread 7 | Config.MapLoadTimeout = 10000 8 | 9 | -- Maximum number of entities that may be spawned at the same time 10 | Config.MaxEntities = 300 11 | -------------------------------------------------------------------------------- /[examples]/example2/fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'adamant' 2 | game '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 | dependency 'objectloader' 6 | 7 | files { 8 | 'map1.xml', 9 | 'map2.xml' 10 | } 11 | 12 | objectloader_maps { 13 | 'map1.xml', 14 | 'map2.xml' 15 | } 16 | -------------------------------------------------------------------------------- /[examples]/example2/map2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /[examples]/example2/map1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedM Object Loader 2 | 3 | Loads map XML files exported from the RDR2 Map Editor by Lambdarevolution: https://allmods.net/red-dead-redemption-2/tools-red-dead-redemption-2/rdr2-map-editor-v0-10/ 4 | 5 | [![Butter Bridge](https://i.imgur.com/qlmvzwdm.jpg)](https://imgur.com/qlmvzwd) 6 | 7 | ## Installing 8 | 9 | Place the files inside a subfolder in the resources directory, for example: 10 | 11 | ``` 12 | resources/[local]/[objectloader] 13 | resources/[local]/[objectloader]/[maps] 14 | resources/[local]/[objectloader]/objectloader 15 | ``` 16 | 17 | The `[ ]` in the names are important: only folders with names like `[this]` will be searched by the server for resources. 18 | 19 | You do not need to add `ensure objectloader` to `server.cfg`. Each map should include `objectloader` as a dependency, which will automatically start it if it is not already running. 20 | 21 | ## Adding a map 22 | 23 | 1. Create a new resource for the map: 24 | 25 | `resources/[local]/[objectloader]/[maps]/mymap`. 26 | 27 | 2. Copy the map editor XML file into the resource folder: 28 | 29 | `resources/[local]/[objectloader]/[maps]/mymap/mymap.xml` 30 | 31 | 3. Create a resource manifest (`fxmanifest.lua`), and enter the following inside it: 32 | 33 | For a single map editor XML file: 34 | ``` 35 | fx_version 'adamant' 36 | game 'rdr3' 37 | rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' 38 | 39 | dependency 'objectloader' 40 | 41 | file 'mymap.xml' 42 | 43 | objectloader_map 'mymap.xml' 44 | ``` 45 | 46 | For multiple map editor XML files: 47 | ``` 48 | fx_version 'adamant' 49 | game 'rdr3' 50 | rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' 51 | 52 | dependency 'objectloader' 53 | 54 | files { 55 | 'mymap1.xml', 56 | 'mymap2.xml' 57 | } 58 | 59 | objectloader_maps { 60 | 'mymap1.xml', 61 | 'mymap2.xml' 62 | } 63 | ``` 64 | 65 | 4. Add `ensure mymap` inside `server.cfg`. 66 | 67 | 5. To enable the map immediately without restarting the server, do the following in the console: 68 | 69 | ``` 70 | refresh 71 | ensure mymap 72 | ``` 73 | -------------------------------------------------------------------------------- /objectloader/slaxml.lua: -------------------------------------------------------------------------------- 1 | --[=====================================================================[ 2 | v0.8 Copyright © 2013-2018 Gavin Kistner ; MIT Licensed 3 | See http://github.com/Phrogz/SLAXML for details. 4 | --]=====================================================================] 5 | SLAXML = { 6 | VERSION = "0.8", 7 | _call = { 8 | pi = function(target,content) 9 | print(string.format("",target,content)) 10 | end, 11 | comment = function(content) 12 | print(string.format("",content)) 13 | end, 14 | startElement = function(name,nsURI,nsPrefix) 15 | io.write("<") 16 | if nsPrefix then io.write(nsPrefix,":") end 17 | io.write(name) 18 | if nsURI then io.write(" (ns='",nsURI,"')") end 19 | print(">") 20 | end, 21 | attribute = function(name,value,nsURI,nsPrefix) 22 | io.write(' ') 23 | if nsPrefix then io.write(nsPrefix,":") end 24 | io.write(name,'=',string.format('%q',value)) 25 | if nsURI then io.write(" (ns='",nsURI,"')") end 26 | io.write("\n") 27 | end, 28 | text = function(text,cdata) 29 | print(string.format(" %s: %q",cdata and 'cdata' or 'text',text)) 30 | end, 31 | closeElement = function(name,nsURI,nsPrefix) 32 | io.write("") 35 | end, 36 | } 37 | } 38 | 39 | function SLAXML:parser(callbacks) 40 | return { _call=callbacks or self._call, parse=SLAXML.parse } 41 | end 42 | 43 | function SLAXML:parse(xml,options) 44 | if not options then options = { stripWhitespace=false } end 45 | 46 | -- Cache references for maximum speed 47 | local find, sub, gsub, char, push, pop, concat = string.find, string.sub, string.gsub, string.char, table.insert, table.remove, table.concat 48 | local first, last, match1, match2, match3, pos2, nsURI 49 | local unpack = unpack or table.unpack 50 | local pos = 1 51 | local state = "text" 52 | local textStart = 1 53 | local currentElement={} 54 | local currentAttributes={} 55 | local currentAttributeCt -- manually track length since the table is re-used 56 | local nsStack = {} 57 | local anyElement = false 58 | 59 | local utf8markers = { {0x7FF,192}, {0xFFFF,224}, {0x1FFFFF,240} } 60 | local function utf8(decimal) -- convert unicode code point to utf-8 encoded character string 61 | if decimal<128 then return char(decimal) end 62 | local charbytes = {} 63 | for bytes,vals in ipairs(utf8markers) do 64 | if decimal<=vals[1] then 65 | for b=bytes+1,2,-1 do 66 | local mod = decimal%64 67 | decimal = (decimal-mod)/64 68 | charbytes[b] = char(128+mod) 69 | end 70 | charbytes[1] = char(vals[2]+decimal) 71 | return concat(charbytes) 72 | end 73 | end 74 | end 75 | local entityMap = { ["lt"]="<", ["gt"]=">", ["amp"]="&", ["quot"]='"', ["apos"]="'" } 76 | local entitySwap = function(orig,n,s) return entityMap[s] or n=="#" and utf8(tonumber('0'..s)) or orig end 77 | local function unescape(str) return gsub( str, '(&(#?)([%d%a]+);)', entitySwap ) end 78 | 79 | local function finishText() 80 | if first>textStart and self._call.text then 81 | local text = sub(xml,textStart,first-1) 82 | if options.stripWhitespace then 83 | text = gsub(text,'^%s+','') 84 | text = gsub(text,'%s+$','') 85 | if #text==0 then text=nil end 86 | end 87 | if text then self._call.text(unescape(text),false) end 88 | end 89 | end 90 | 91 | local function findPI() 92 | first, last, match1, match2 = find( xml, '^<%?([:%a_][:%w_.-]*) ?(.-)%?>', pos ) 93 | if first then 94 | finishText() 95 | if self._call.pi then self._call.pi(match1,match2) end 96 | pos = last+1 97 | textStart = pos 98 | return true 99 | end 100 | end 101 | 102 | local function findComment() 103 | first, last, match1 = find( xml, '^', pos ) 104 | if first then 105 | finishText() 106 | if self._call.comment then self._call.comment(match1) end 107 | pos = last+1 108 | textStart = pos 109 | return true 110 | end 111 | end 112 | 113 | local function nsForPrefix(prefix) 114 | if prefix=='xml' then return 'http://www.w3.org/XML/1998/namespace' end -- http://www.w3.org/TR/xml-names/#ns-decl 115 | for i=#nsStack,1,-1 do if nsStack[i][prefix] then return nsStack[i][prefix] end end 116 | error(("Cannot find namespace for prefix %s"):format(prefix)) 117 | end 118 | 119 | local function startElement() 120 | anyElement = true 121 | first, last, match1 = find( xml, '^<([%a_][%w_.-]*)', pos ) 122 | if first then 123 | currentElement[2] = nil -- reset the nsURI, since this table is re-used 124 | currentElement[3] = nil -- reset the nsPrefix, since this table is re-used 125 | finishText() 126 | pos = last+1 127 | first,last,match2 = find(xml, '^:([%a_][%w_.-]*)', pos ) 128 | if first then 129 | currentElement[1] = match2 130 | currentElement[3] = match1 -- Save the prefix for later resolution 131 | match1 = match2 132 | pos = last+1 133 | else 134 | currentElement[1] = match1 135 | for i=#nsStack,1,-1 do if nsStack[i]['!'] then currentElement[2] = nsStack[i]['!']; break end end 136 | end 137 | currentAttributeCt = 0 138 | push(nsStack,{}) 139 | return true 140 | end 141 | end 142 | 143 | local function findAttribute() 144 | first, last, match1 = find( xml, '^%s+([:%a_][:%w_.-]*)%s*=%s*', pos ) 145 | if first then 146 | pos2 = last+1 147 | first, last, match2 = find( xml, '^"([^<"]*)"', pos2 ) -- FIXME: disallow non-entity ampersands 148 | if first then 149 | pos = last+1 150 | match2 = unescape(match2) 151 | else 152 | first, last, match2 = find( xml, "^'([^<']*)'", pos2 ) -- FIXME: disallow non-entity ampersands 153 | if first then 154 | pos = last+1 155 | match2 = unescape(match2) 156 | end 157 | end 158 | end 159 | if match1 and match2 then 160 | local currentAttribute = {match1,match2} 161 | local prefix,name = string.match(match1,'^([^:]+):([^:]+)$') 162 | if prefix then 163 | if prefix=='xmlns' then 164 | nsStack[#nsStack][name] = match2 165 | else 166 | currentAttribute[1] = name 167 | currentAttribute[4] = prefix 168 | end 169 | else 170 | if match1=='xmlns' then 171 | nsStack[#nsStack]['!'] = match2 172 | currentElement[2] = match2 173 | end 174 | end 175 | currentAttributeCt = currentAttributeCt + 1 176 | currentAttributes[currentAttributeCt] = currentAttribute 177 | return true 178 | end 179 | end 180 | 181 | local function findCDATA() 182 | first, last, match1 = find( xml, '^', pos ) 183 | if first then 184 | finishText() 185 | if self._call.text then self._call.text(match1,true) end 186 | pos = last+1 187 | textStart = pos 188 | return true 189 | end 190 | end 191 | 192 | local function closeElement() 193 | first, last, match1 = find( xml, '^%s*(/?)>', pos ) 194 | if first then 195 | state = "text" 196 | pos = last+1 197 | textStart = pos 198 | 199 | -- Resolve namespace prefixes AFTER all new/redefined prefixes have been parsed 200 | if currentElement[3] then currentElement[2] = nsForPrefix(currentElement[3]) end 201 | if self._call.startElement then self._call.startElement(unpack(currentElement)) end 202 | if self._call.attribute then 203 | for i=1,currentAttributeCt do 204 | if currentAttributes[i][4] then currentAttributes[i][3] = nsForPrefix(currentAttributes[i][4]) end 205 | self._call.attribute(unpack(currentAttributes[i])) 206 | end 207 | end 208 | 209 | if match1=="/" then 210 | pop(nsStack) 211 | if self._call.closeElement then self._call.closeElement(unpack(currentElement)) end 212 | end 213 | return true 214 | end 215 | end 216 | 217 | local function findElementClose() 218 | first, last, match1, match2 = find( xml, '^', pos ) 219 | if first then 220 | nsURI = nil 221 | for i=#nsStack,1,-1 do if nsStack[i]['!'] then nsURI = nsStack[i]['!']; break end end 222 | else 223 | first, last, match2, match1 = find( xml, '^', pos ) 224 | if first then nsURI = nsForPrefix(match2) end 225 | end 226 | if first then 227 | finishText() 228 | if self._call.closeElement then self._call.closeElement(match1,nsURI) end 229 | pos = last+1 230 | textStart = pos 231 | pop(nsStack) 232 | return true 233 | end 234 | end 235 | 236 | while pos<#xml do 237 | if state=="text" then 238 | if not (findPI() or findComment() or findCDATA() or findElementClose()) then 239 | if startElement() then 240 | state = "attributes" 241 | else 242 | first, last = find( xml, '^[^<]+', pos ) 243 | pos = (first and last or pos) + 1 244 | end 245 | end 246 | elseif state=="attributes" then 247 | if not findAttribute() then 248 | if not closeElement() then 249 | error("Was in an element and couldn't find attributes or the close.") 250 | end 251 | end 252 | end 253 | end 254 | 255 | if not anyElement then error("Parsing did not discover any elements") end 256 | if #nsStack > 0 then error("Parsing ended with unclosed elements") end 257 | end 258 | 259 | -- Optional parser that creates a flat DOM from parsing 260 | function SLAXML:dom(xml,opts) 261 | if not opts then opts={} end 262 | local rich = not opts.simple 263 | local push, pop = table.insert, table.remove 264 | local doc = {type="document", name="#doc", kids={}} 265 | local current,stack = doc, {doc} 266 | local builder = SLAXML:parser{ 267 | startElement = function(name,nsURI,nsPrefix) 268 | local el = { type="element", name=name, kids={}, el=rich and {} or nil, attr={}, nsURI=nsURI, nsPrefix=nsPrefix, parent=rich and current or nil } 269 | if current==doc then 270 | if doc.root then error(("Encountered element '%s' when the document already has a root '%s' element"):format(name,doc.root.name)) end 271 | doc.root = rich and el or nil 272 | end 273 | push(current.kids,el) 274 | if current.el then push(current.el,el) end 275 | current = el 276 | push(stack,el) 277 | end, 278 | attribute = function(name,value,nsURI,nsPrefix) 279 | if not current or current.type~="element" then error(("Encountered an attribute %s=%s but I wasn't inside an element"):format(name,value)) end 280 | local attr = {type='attribute',name=name,nsURI=nsURI,nsPrefix=nsPrefix,value=value,parent=rich and current or nil} 281 | if rich then current.attr[name] = value end 282 | push(current.attr,attr) 283 | end, 284 | closeElement = function(name) 285 | if current.name~=name or current.type~="element" then error(("Received a close element notification for '%s' but was inside a '%s' %s"):format(name,current.name,current.type)) end 286 | pop(stack) 287 | current = stack[#stack] 288 | end, 289 | text = function(value,cdata) 290 | -- documents may only have text node children that are whitespace: https://www.w3.org/TR/xml/#NT-Misc 291 | if current.type=='document' and not value:find('^%s+$') then error(("Document has non-whitespace text at root: '%s'"):format(value:gsub('[\r\n\t]',{['\r']='\\r', ['\n']='\\n', ['\t']='\\t'}))) end 292 | push(current.kids,{type='text',name='#text',cdata=cdata and true or nil,value=value,parent=rich and current or nil}) 293 | end, 294 | comment = function(value) 295 | push(current.kids,{type='comment',name='#comment',value=value,parent=rich and current or nil}) 296 | end, 297 | pi = function(name,value) 298 | push(current.kids,{type='pi',name=name,value=value,parent=rich and current or nil}) 299 | end 300 | } 301 | builder:parse(xml,opts) 302 | return doc 303 | end 304 | 305 | local escmap = {["<"]="<", [">"]=">", ["&"]="&", ['"']=""", ["'"]="'"} 306 | local function esc(s) return s:gsub('[<>&"]', escmap) end 307 | 308 | -- opts.indent: number of spaces, or string 309 | function SLAXML:xml(n,opts) 310 | opts = opts or {} 311 | local out = {} 312 | local tab = opts.indent and (type(opts.indent)=="number" and string.rep(" ",opts.indent) or opts.indent) or "" 313 | local ser = {} 314 | local omit = {} 315 | if opts.omit then for _,s in ipairs(opts.omit) do omit[s]=true end end 316 | 317 | function ser.document(n) 318 | for _,kid in ipairs(n.kids) do 319 | if ser[kid.type] then ser[kid.type](kid,0) end 320 | end 321 | end 322 | 323 | function ser.pi(n,depth) 324 | depth = depth or 0 325 | table.insert(out, tab:rep(depth)..'') 326 | end 327 | 328 | function ser.element(n,depth) 329 | if n.nsURI and omit[n.nsURI] then return end 330 | depth = depth or 0 331 | local indent = tab:rep(depth) 332 | local name = n.nsPrefix and n.nsPrefix..':'..n.name or n.name 333 | local result = indent..'<'..name 334 | if n.attr and n.attr[1] then 335 | local sorted = n.attr 336 | if opts.sort then 337 | sorted = {} 338 | for i,a in ipairs(n.attr) do sorted[i]=a end 339 | table.sort(sorted,function(a,b) 340 | if a.nsPrefix and b.nsPrefix then 341 | return a.nsPrefix==b.nsPrefix and a.name' or '/>') 361 | table.insert(out, result) 362 | if n.kids and n.kids[1] then 363 | for _,kid in ipairs(n.kids) do 364 | if ser[kid.type] then ser[kid.type](kid,depth+1) end 365 | end 366 | table.insert(out, indent..'') 367 | end 368 | end 369 | 370 | function ser.text(n,depth) 371 | if n.cdata then 372 | table.insert(out, tab:rep(depth)..'') 373 | else 374 | table.insert(out, tab:rep(depth)..esc(n.value)) 375 | end 376 | end 377 | 378 | function ser.comment(n,depth) 379 | table.insert(out, tab:rep(depth)..'') 380 | end 381 | 382 | ser[n.type](n,0) 383 | 384 | return table.concat(out, opts.indent and '\n' or '') 385 | end 386 | 387 | return SLAXML 388 | -------------------------------------------------------------------------------- /objectloader/client.lua: -------------------------------------------------------------------------------- 1 | local Maps = {} 2 | 3 | local TotalEntities = 0 4 | 5 | function GetDistance(object, myPos) 6 | return #(myPos - vector3(object.Position_x, object.Position_y, object.Position_z)) 7 | end 8 | 9 | function IsNearby(object, myPos) 10 | return GetDistance(object, myPos) <= Config.SpawnDistance 11 | end 12 | 13 | function values(t) 14 | local i = 0 15 | return function() 16 | if t then 17 | i = i + 1 18 | return t[i] 19 | else 20 | return nil 21 | end 22 | end 23 | end 24 | 25 | function LoadModel(model) 26 | if IsModelInCdimage(model) then 27 | RequestModel(model) 28 | 29 | while not HasModelLoaded(model) do 30 | Wait(0) 31 | end 32 | 33 | return true 34 | else 35 | print('Error: Model does not exist: ' .. model) 36 | return false 37 | end 38 | end 39 | 40 | function SpawnObject(object) 41 | if not LoadModel(object.Hash) then 42 | return false 43 | end 44 | 45 | object.handle = CreateObjectNoOffset( 46 | object.Hash, 47 | object.Position_x, 48 | object.Position_y, 49 | object.Position_z, 50 | false, -- isNetwork 51 | false, -- netMissionEntity 52 | object.Dynamic, 53 | false) 54 | 55 | SetModelAsNoLongerNeeded(object.Hash) 56 | 57 | if object.handle == 0 then 58 | return false 59 | end 60 | 61 | FreezeEntityPosition(object.handle, true) 62 | 63 | SetEntityRotation(object.handle, object.Rotation_x, object.Rotation_y, object.Rotation_z, 0, false) 64 | 65 | if object.LOD then 66 | SetEntityLodDist(object.handle, object.LOD) 67 | else 68 | SetEntityLodDist(object.handle, 0xFFFF) 69 | end 70 | 71 | if object.Collision ~= nil then 72 | SetEntityCollision(object.handle, object.Collision) 73 | end 74 | 75 | if object.Visible ~= nil then 76 | SetEntityVisible(object.handle, object.Visible) 77 | end 78 | 79 | return true 80 | end 81 | 82 | function ClearObject(object) 83 | DeleteObject(object.handle) 84 | object.handle = nil 85 | end 86 | 87 | function RemoveDeletedObject(object) 88 | local handle = GetClosestObjectOfType(object.Position_x, object.Position_y, object.Position_z, 1.0, object.Hash, false, false, false) 89 | 90 | if handle ~= 0 then 91 | DeleteObject(handle) 92 | end 93 | end 94 | 95 | function SetRandomOutfitVariation(ped, p1) 96 | Citizen.InvokeNative(0x283978A15512B2FE, ped, p1) 97 | end 98 | 99 | function SpawnPed(ped) 100 | if not LoadModel(ped.Hash) then 101 | return false 102 | end 103 | 104 | ped.handle = CreatePed( 105 | ped.Hash, 106 | ped.Position_x, 107 | ped.Position_y, 108 | ped.Position_z, 109 | 0.0, 110 | false, -- isNetwork 111 | false, -- netMissionEntity 112 | false, 113 | false) 114 | 115 | SetModelAsNoLongerNeeded(ped.Hash) 116 | 117 | if ped.handle == 0 then 118 | return false 119 | end 120 | 121 | FreezeEntityPosition(ped.handle, true) 122 | 123 | SetEntityRotation(ped.handle, ped.Rotation_x, ped.Rotation_y, ped.Rotation_z, 0, false) 124 | 125 | if ped.Collision ~= nil then 126 | SetEntityCollision(ped.handle, ped.Collision) 127 | end 128 | 129 | if ped.Visible ~= nil then 130 | SetEntityVisible(ped.handle, ped.Visible) 131 | end 132 | 133 | if not ped.Preset or ped.Preset == -1 then 134 | SetRandomOutfitVariation(ped.handle, true) 135 | else 136 | SetPedOutfitPreset(ped.handle, ped.Preset, 0) 137 | end 138 | 139 | if ped.WeaponHash then 140 | GiveWeaponToPed_2(ped.handle, ped.WeaponHash, 500, true, false, 0, false, 0.5, 1.0, 0, false, 0.0, false) 141 | end 142 | 143 | if ped.Scenario then 144 | TaskStartScenarioInPlace(ped.handle, GetHashKey(ped.Scenario), 0, true) 145 | end 146 | 147 | return true 148 | end 149 | 150 | function ClearPed(ped) 151 | DeletePed(ped.handle) 152 | ped.handle = nil 153 | end 154 | 155 | function SpawnVehicle(vehicle) 156 | if not LoadModel(vehicle.Hash) then 157 | return false 158 | end 159 | 160 | vehicle.handle = CreateVehicle( 161 | vehicle.Hash, 162 | vehicle.Position_x, 163 | vehicle.Position_y, 164 | vehicle.Position_z, 165 | 0.0, 166 | false, -- isNetwork 167 | false, -- netMissionEntity 168 | false, 169 | false) 170 | 171 | SetModelAsNoLongerNeeded(vehicle.Hash) 172 | 173 | if vehicle.handle == 0 then 174 | return false 175 | end 176 | 177 | FreezeEntityPosition(vehicle.handle, true) 178 | 179 | SetEntityRotation(vehicle.handle, vehicle.Rotation_x, vehicle.Rotation_y, vehicle.Rotation_z, 0, false) 180 | 181 | if vehicle.Collision ~= nil then 182 | SetEntityCollision(vehicle.handle, vehicle.Collision) 183 | end 184 | 185 | if vehicle.Visible ~= nil then 186 | SetEntityVisible(vehicle.handle, vehicle.Visible) 187 | end 188 | 189 | return true 190 | end 191 | 192 | function ClearVehicle(vehicle) 193 | DeleteVehicle(vehicle.handle) 194 | vehicle.handle = nil 195 | end 196 | 197 | function SpawnPickup(pickup) 198 | if not LoadModel(pickup.ModelHash) then 199 | return false 200 | end 201 | 202 | pickup.handle = CreatePickup( 203 | pickup.PickupHash, 204 | pickup.Position_x, 205 | pickup.Position_y, 206 | pickup.Position_z, 207 | 0, 208 | 0, 209 | false, 210 | pickup.ModelHash, 211 | 0, 212 | 0.0, 213 | 0) 214 | 215 | SetModelAsNoLongerNeeded(pickup.ModelHash) 216 | 217 | if pickup.handle == 0 then 218 | return false 219 | end 220 | 221 | return true 222 | end 223 | 224 | function ClearPickup(pickup) 225 | DeleteEntity(pickup.handle) 226 | pickup.handle = nil 227 | end 228 | 229 | function UpdateEntity(entity, myPos, spawnFunc, clearFunc) 230 | if not DoesEntityExist(entity.handle) then 231 | entity.handle = nil 232 | end 233 | 234 | local nearby = IsNearby(entity, myPos) 235 | 236 | if nearby and not entity.handle then 237 | if TotalEntities < Config.MaxEntities then 238 | if spawnFunc(entity) then 239 | TotalEntities = TotalEntities + 1 240 | end 241 | end 242 | elseif not nearby and entity.handle then 243 | clearFunc(entity) 244 | 245 | if TotalEntities > 0 then 246 | TotalEntities = TotalEntities - 1 247 | end 248 | end 249 | end 250 | 251 | function UpdateMap(map) 252 | local myPos = GetEntityCoords(PlayerPedId()) 253 | 254 | for object in values(map.DeletedObject) do 255 | if IsNearby(object, myPos) then 256 | RemoveDeletedObject(object) 257 | end 258 | end 259 | 260 | for object in values(map.Object) do 261 | UpdateEntity(object, myPos, SpawnObject, ClearObject) 262 | end 263 | 264 | for pickup in values(map.PickupObject) do 265 | UpdateEntity(pickup, myPos, SpawnPickup, ClearPickup) 266 | end 267 | 268 | for ped in values(map.Ped) do 269 | UpdateEntity(ped, myPos, SpawnPed, ClearPed) 270 | end 271 | 272 | for vehicle in values(map.Vehicle) do 273 | UpdateEntity(vehicle, myPos, SpawnVehicle, ClearVehicle) 274 | end 275 | end 276 | 277 | function ClearMap(map) 278 | for object in values(map.Object) do 279 | ClearObject(object) 280 | 281 | if TotalEntities > 0 then 282 | TotalEntities = TotalEntities - 1 283 | end 284 | end 285 | 286 | for pickup in values(map.PickupObject) do 287 | ClearPickup(pickup) 288 | 289 | if TotalEntities > 0 then 290 | TotalEntities = TotalEntities - 1 291 | end 292 | end 293 | 294 | for ped in values(map.Ped) do 295 | ClearPed(ped) 296 | 297 | if TotalEntities > 0 then 298 | TotalEntities = TotalEntities - 1 299 | end 300 | end 301 | 302 | for vehicle in values(map.Vehicle) do 303 | ClearVehicle(vehicle) 304 | 305 | if TotalEntities > 0 then 306 | TotalEntities = TotalEntities - 1 307 | end 308 | end 309 | end 310 | 311 | function CreateMapThread(name) 312 | CreateThread(function() 313 | Maps[name].enabled = true 314 | Maps[name].unloaded = false 315 | 316 | while Maps[name] and Maps[name].enabled do 317 | Maps[name].lastUpdated = GetSystemTime() 318 | UpdateMap(Maps[name]) 319 | Wait(500) 320 | end 321 | 322 | ClearMap(Maps[name]) 323 | Maps[name].unloaded = true 324 | end) 325 | end 326 | 327 | local function enableMap(name) 328 | if Maps[name] and not Maps[name].enabled then 329 | CreateMapThread(name) 330 | end 331 | end 332 | 333 | local function disableMap(name) 334 | if Maps[name] and Maps[name].enabled then 335 | Maps[name].enabled = false 336 | 337 | while Maps[name] and not Maps[name].unloaded do 338 | Citizen.Wait(0) 339 | end 340 | end 341 | end 342 | 343 | function InitMap(name, map, enabled) 344 | if Maps[name] then 345 | RemoveMap(name) 346 | end 347 | 348 | Maps[name] = map 349 | 350 | local uniqueCreators = {} 351 | 352 | if map.MapMeta then 353 | for _, meta in ipairs(map.MapMeta) do 354 | if meta.Creator then 355 | uniqueCreators[meta.Creator] = true 356 | end 357 | end 358 | end 359 | 360 | local creators = {} 361 | 362 | for creator, _ in pairs(uniqueCreators) do 363 | table.insert(creators, creator) 364 | end 365 | 366 | if #creators > 0 then 367 | print("Added map " .. name .. " by " .. table.concat(creators, ", ")) 368 | else 369 | print("Added map " .. name) 370 | end 371 | 372 | if enabled then 373 | enableMap(name) 374 | end 375 | end 376 | 377 | function RemoveMap(name) 378 | if Maps[name] then 379 | if Maps[name].enabled then 380 | disableMap(name) 381 | end 382 | 383 | Maps[name] = nil 384 | 385 | print('Removed map ' .. name) 386 | else 387 | print('No map named ' .. name .. ' loaded') 388 | end 389 | end 390 | 391 | function ToNumber(value) 392 | return tonumber(value) 393 | end 394 | 395 | function ToBoolean(value) 396 | return value == 'true' 397 | end 398 | 399 | function ToFloat(value) 400 | return tonumber(value) + 0.0 401 | end 402 | 403 | local AttributeTypes = { 404 | ['Collision'] = ToBoolean, 405 | ['Dynamic'] = ToBoolean, 406 | ['Hash'] = ToNumber, 407 | ['LOD'] = ToNumber, 408 | ['Position_x'] = ToFloat, 409 | ['Position_y'] = ToFloat, 410 | ['Position_z'] = ToFloat, 411 | ['Preset'] = ToNumber, 412 | ['Rotation_x'] = ToFloat, 413 | ['Rotation_y'] = ToFloat, 414 | ['Rotation_z'] = ToFloat, 415 | ['TextureVariation'] = ToNumber, 416 | ['Visible'] = ToBoolean 417 | } 418 | 419 | function ProcessValue(name, value) 420 | if AttributeTypes[name] then 421 | return AttributeTypes[name](value) 422 | else 423 | return value 424 | end 425 | end 426 | 427 | function ProcessNode(node) 428 | local entity = {} 429 | 430 | for attr in values(node.attr) do 431 | entity[attr.name] = ProcessValue(attr.name, attr.value) 432 | end 433 | 434 | return entity 435 | end 436 | 437 | function AddMaps(name, dataList, enabled) 438 | local map = {} 439 | 440 | for _, data in ipairs(dataList) do 441 | local xml = SLAXML:dom(data) 442 | 443 | for kid in values(xml.root.kids) do 444 | if kid.type == 'element' then 445 | if not map[kid.name] then 446 | map[kid.name] = {} 447 | end 448 | table.insert(map[kid.name], ProcessNode(kid)) 449 | end 450 | end 451 | end 452 | 453 | InitMap(name, map, enabled) 454 | end 455 | 456 | function AddMap(name, data, enabled) 457 | AddMaps(name, {data}, enabled) 458 | end 459 | 460 | local entityEnumerator = { 461 | __gc = function(enum) 462 | if enum.destructor and enum.handle then 463 | enum.destructor(enum.handle) 464 | end 465 | enum.destructor = nil 466 | enum.handle = nil 467 | end 468 | } 469 | 470 | function EnumerateEntities(firstFunc, nextFunc, endFunc) 471 | return coroutine.wrap(function() 472 | local iter, id = firstFunc() 473 | 474 | if not id or id == 0 then 475 | endFunc(iter) 476 | return 477 | end 478 | 479 | local enum = {handle = iter, destructor = endFunc} 480 | setmetatable(enum, entityEnumerator) 481 | 482 | local next = true 483 | repeat 484 | coroutine.yield(id) 485 | next, id = nextFunc(iter) 486 | until not next 487 | 488 | enum.destructor, enum.handle = nil, nil 489 | endFunc(iter) 490 | end) 491 | end 492 | 493 | function EnumerateObjects() 494 | return EnumerateEntities(FindFirstObject, FindNextObject, EndFindObject) 495 | end 496 | 497 | function EnumeratePeds() 498 | return EnumerateEntities(FindFirstPed, FindNextPed, EndFindPed) 499 | end 500 | 501 | function EnumerateVehicles() 502 | return EnumerateEntities(FindFirstVehicle, FindNextVehicle, EndFindVehicle) 503 | end 504 | 505 | AddEventHandler('onClientResourceStart', function(resourceName) 506 | local numMaps = GetNumResourceMetadata(resourceName, 'objectloader_map') 507 | 508 | if not numMaps or numMaps < 1 then 509 | return 510 | end 511 | 512 | local dataList = {} 513 | 514 | for i = 0, numMaps - 1 do 515 | local fileName = GetResourceMetadata(resourceName, 'objectloader_map', i) 516 | local data = LoadResourceFile(resourceName, fileName) 517 | table.insert(dataList, data) 518 | end 519 | 520 | local enabled = GetResourceMetadata(resourceName, 'objectloader_enabled', 0) 521 | 522 | AddMaps(resourceName, dataList, enabled ~= "no") 523 | end) 524 | 525 | AddEventHandler('onResourceStop', function(resourceName) 526 | if GetCurrentResourceName() == resourceName then 527 | for name, map in pairs(Maps) do 528 | ClearMap(map) 529 | end 530 | elseif Maps[resourceName] then 531 | RemoveMap(resourceName) 532 | end 533 | end) 534 | 535 | function HasMapFailed(name) 536 | return Maps[name] and Maps[name].lastUpdated and GetSystemTime() - Maps[name].lastUpdated > Config.MapLoadTimeout 537 | end 538 | 539 | function CheckMaps() 540 | for name, map in pairs(Maps) do 541 | if map.enabled and HasMapFailed(name) then 542 | print('Restarting map ' .. name .. '...') 543 | ClearMap(Maps[name]) 544 | CreateMapThread(name) 545 | end 546 | end 547 | end 548 | 549 | exports('addMap', AddMap) 550 | exports('removeMap', RemoveMap) 551 | exports('enableMap', enableMap) 552 | exports('disableMap', disableMap) 553 | 554 | CreateThread(function() 555 | while true do 556 | CheckMaps() 557 | Wait(0) 558 | end 559 | end) 560 | 561 | CreateThread(function() 562 | while true do 563 | if TotalEntities >= Config.MaxEntities then 564 | print("Max entity limit (" .. Config.MaxEntities .. ") has been reached. Please reduce the number of entities in your maps.") 565 | Wait(60000) 566 | else 567 | Wait(1000) 568 | end 569 | end 570 | end) 571 | 572 | local DebugMode = false 573 | 574 | function DrawText(text, x, y) 575 | SetTextScale(0.35, 0.35) 576 | SetTextColor(255, 255, 255, 255) 577 | SetTextDropshadow(1, 0, 0, 0, 200) 578 | SetTextFontForCurrentCommand(0) 579 | DisplayText(CreateVarString(10, "LITERAL_STRING", text), x, y) 580 | end 581 | 582 | RegisterCommand("objectloader_debug", function() 583 | DebugMode = not DebugMode 584 | end) 585 | 586 | CreateThread(function() 587 | while true do 588 | if DebugMode then 589 | local totalMaps = 0 590 | for name, _ in pairs(Maps) do 591 | totalMaps = totalMaps + 1 592 | end 593 | DrawText("Maps loaded: " .. totalMaps, 0.85, 0.03) 594 | DrawText("Entities spawned: " .. TotalEntities, 0.85, 0.06) 595 | Wait(0) 596 | else 597 | Wait(500) 598 | end 599 | end 600 | end) 601 | -------------------------------------------------------------------------------- /[examples]/example1/butterbridge.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | --------------------------------------------------------------------------------