├── [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 |
16 |
--------------------------------------------------------------------------------
/[examples]/example2/map1.xml:
--------------------------------------------------------------------------------
1 |
2 |
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 | [](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("%s %s?>",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("")
33 | if nsPrefix then io.write(nsPrefix,":") end
34 | print(name..">")
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, '^([%a_][%w_.-]*)%s*>', 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, '^([%a_][%w_.-]*):([%a_][%w_.-]*)%s*>', 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)..''..n.name..' '..n.value..'?>')
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..''..name..'>')
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 |
295 |
--------------------------------------------------------------------------------