1-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
2-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
3
4local utils = require 'utils'
5local Serializer = require 'Serializer'
6--
7-- Class: EquipSet
8--
9-- A container for a ship's equipment.
10local EquipSet = utils.inherits(nil, "EquipSet")
11
12EquipSet.default = {
13	cargo=0,
14	engine=1,
15	laser_front=1,
16	laser_rear=0,
17	missile=0,
18	ecm=1,
19	radar=1,
20	target_scanner=1,
21	hypercloud=1,
22	hull_autorepair=1,
23	energy_booster=1,
24	atmo_shield=1,
25	cabin=50,
26	shield=9999,
27	scoop=2,
28	laser_cooler=1,
29	cargo_life_support=1,
30	autopilot=1,
31	trade_computer=1,
32	sensor = 8,
33	thruster = 1
34}
35
36function EquipSet.New (slots)
37	local obj = {}
38	obj.slots = {}
39	for k, n in pairs(EquipSet.default) do
40		obj.slots[k] = {__occupied = 0, __limit = n}
41	end
42	for k, n in pairs(slots) do
43		obj.slots[k] = {__occupied = 0, __limit = n}
44	end
45	setmetatable(obj, EquipSet.meta)
46	return obj
47end
48
49local listeners = {}
50function EquipSet:AddListener(listener)
51	listeners[self] = listener
52end
53
54function EquipSet:CallListener(slot)
55	if listeners[self] then
56		listeners[self](slot)
57	end
58end
59
60-- XXX(sturnclaw): to fix massive save-file inflation, we manually coalesce cargo items
61-- This is suboptimal; cargo should be logically different from ship equipment
62function EquipSet:Serialize()
63	local serialize = {
64		slots = {}
65	}
66
67	for k, v in pairs(self.slots) do
68		serialize.slots[k] = v
69	end
70
71	serialize.slots.cargo = {
72		__limit = self.slots.cargo.__limit,
73		__occupied = self.slots.cargo.__occupied,
74		__version = 2
75	}
76
77	-- count the number of cargo items in the bay
78	local occupancy = {}
79	for _, v in pairs(self.slots.cargo) do
80		if type (_) == "number" and type(v) == "table" then
81			occupancy[v] = (occupancy[v] or 0) + 1
82		end
83	end
84
85	-- Collapse instances of the same cargo item into one
86	for k, v in pairs(occupancy) do
87		table.insert(serialize.slots.cargo, { item = k, count = v })
88	end
89
90	return serialize
91end
92
93function EquipSet.Unserialize(data)
94	local cargo = data.slots.cargo
95
96	if (cargo.__version or 0) >= 2 then
97		local newCargo = {
98			__limit = cargo.__limit,
99			__occupied = cargo.__occupied
100		}
101
102		-- unpack collapsed cargo items
103		for _, v in ipairs(cargo) do
104			for i = 1, v.count do table.insert(newCargo, v.item) end
105		end
106
107		data.slots.cargo = newCargo
108	end
109
110	setmetatable(data, EquipSet.meta)
111	return data
112end
113
114--
115-- Group: Methods
116--
117
118--
119-- Method: FreeSpace
120--
121--  returns the available space in the given slot.
122--
123-- Parameters:
124--
125--  slot - The slot name.
126--
127-- Return:
128--
129--  free_space - The available space (integer)
130--
131function EquipSet:FreeSpace (slot)
132	local s = self.slots[slot]
133	if not s then
134		return 0
135	end
136	return s.__limit - s.__occupied
137end
138
139function EquipSet:SlotSize(slot)
140	local s = self.slots[slot]
141	if not s then
142		return 0
143	end
144	return s.__limit
145end
146
147--
148-- Method: OccupiedSpace
149--
150--  returns the space occupied in the given slot.
151--
152-- Parameters:
153--
154--  slot - The slot name.
155--
156-- Return:
157--
158--  occupied_space - The occupied space (integer)
159--
160function EquipSet:OccupiedSpace (slot)
161	local s = self.slots[slot]
162	if not s then
163		return 0
164	end
165	return s.__occupied
166end
167
168--
169-- Method: Count
170--
171--  returns the number of occurrences of the given equipment in the specified slot.
172--
173-- Parameters:
174--
175--  item - The equipment to count.
176--
177--  slots - List of the slots to check. You can also provide a string if it
178--          is only one slot. If this argument is not provided, all slots
179--          will be searched.
180--
181-- Return:
182--
183--  free_space - The available space (integer)
184--
185function EquipSet:Count(item, slots)
186	local to_check
187	if type(slots) == "table" then
188		to_check = {}
189		for _, s in ipairs(slots) do
190			table.insert(to_check, self.slots[s])
191		end
192	elseif slots == nil then
193		to_check = self.slots
194	else
195		to_check = {self.slots[slots]}
196	end
197
198	local count = 0
199	for _, slot in pairs(to_check) do
200		for _, e in pairs(slot) do
201			if e == item then
202				count = count + 1
203			end
204		end
205	end
206	return count
207end
208
209function EquipSet:__TriggerCallbacks(ship, slot)
210	ship:UpdateEquipStats()
211	if slot == "cargo" then -- TODO: build a proper property system for the slots
212		ship:setprop("usedCargo", self.slots.cargo.__occupied)
213	else
214		ship:setprop("totalCargo", math.min(self.slots.cargo.__limit, self.slots.cargo.__occupied+ship.freeCapacity))
215	end
216	self:CallListener(slot)
217end
218
219-- Method: __Remove_NoCheck (PRIVATE)
220--
221--  Remove equipment without checking whether the slot is appropriate nor
222--  calling the uninstall hooks nor even checking the arguments sanity.
223--  It DOES check the free place in the slot.
224--
225-- Parameters:
226--
227--  Please refer to the Remove method.
228--
229-- Return:
230--
231--  Please refer to the Remove method.
232--
233function EquipSet:__Remove_NoCheck (item, num, slot)
234	local s = self.slots[slot]
235	if not s or s.__occupied == 0 then
236		return 0
237	end
238	local removed = 0
239	for i = 1,s.__limit do
240		if removed >= num or s.__occupied <= 0 then
241			return removed
242		end
243		if s[i] == item then
244			s[i] = nil
245			removed = removed + 1
246			s.__occupied = s.__occupied - 1
247		end
248	end
249	return removed
250end
251
252-- Method: __Add_NoCheck (PRIVATE)
253--
254--  Add equipment without checking whether the slot is appropriate nor
255--  calling the install hooks nor even checking the arguments sanity.
256--  It DOES check the free place in the slot.
257--
258-- Parameters:
259--
260--  Please refer to the Add method.
261--
262-- Return:
263--
264--  Please refer to the Add method.
265--
266function EquipSet:__Add_NoCheck(item, num, slot)
267	if self:FreeSpace(slot) == 0 then
268		return 0
269	end
270	local s = self.slots[slot]
271	local added = 0
272	for i = 1,s.__limit do
273		if added >= num or s.__occupied >= s.__limit then
274			return added
275		end
276		if not s[i] then
277			s[i] = item
278			added = added + 1
279			s.__occupied = s.__occupied + 1
280		end
281	end
282	return added
283end
284
285-- Method: Add
286--
287--  Add some equipment to the set, filling the specified slot as much as
288--  possible.
289--
290-- Parameters:
291--
292--  item - the equipment to install
293--  num - the number of pieces to install. If nil, only one will be installed.
294--  slot - the slot where to install the equipment. It will be checked against
295--         the equipment itself, the method will return -1 if the slot isn't
296--         valid. If nil, the default slot for the equipment will be used.
297--
298-- Return:
299--
300--  installed - the number of pieces actually installed, or -1 if the specified
301--              slot is not valid.
302--
303function EquipSet:Add(ship, item, num, slot)
304	num = num or 1
305	if not slot then
306		slot = item:GetDefaultSlot(ship)
307	elseif not item:IsValidSlot(slot, ship) then
308		return -1
309	end
310
311	local added = self:__Add_NoCheck(item, num, slot)
312	if added == 0 then
313		return 0
314	end
315	local postinst_diff = added - item:Install(ship, added, slot)
316	if postinst_diff > 0 then
317		self:__Remove_NoCheck(item, postinst_diff, slot)
318		added = added-postinst_diff
319	end
320	if added > 0 then
321		self:__TriggerCallbacks(ship, slot)
322	end
323	return added
324end
325
326-- Method: Remove
327--
328--  Remove some equipment from the set.
329--
330-- Parameters:
331--
332--  item - the equipment to remove.
333--  num - the number of pieces to uninstall. If nil, only one will be removed.
334--  slot - the slot where to install the equipment. If nil, the default slot
335--         for the equipment will be used.
336--
337-- Return:
338--
339--  removed - the number of pieces actually removed.
340--
341function EquipSet:Remove(ship, item, num, slot)
342	num = num or 1
343	if not slot then
344		slot = item:GetDefaultSlot(ship)
345	end
346	local removed = self:__Remove_NoCheck(item, num, slot)
347	if removed == 0 then
348		return 0
349	end
350	local postuninstall_diff = removed - item:Uninstall(ship, removed, slot)
351	if postuninstall_diff > 0 then
352		self:__Add_NoCheck(item, postuninstall_diff, slot)
353		removed = removed-postuninstall_diff
354	end
355	if removed > 0 then
356		self:__TriggerCallbacks(ship, slot)
357	end
358	return removed
359end
360
361local EquipSet__ClearSlot = function (self, ship, slot)
362	local s = self.slots[slot]
363	local item_counts = {}
364	for k,v in pairs(s) do
365		if type(k) == 'number' then
366			item_counts[v] = (item_counts[v] or 0) + 1
367		end
368	end
369	for item, count in pairs(item_counts) do
370		local uninstalled = item:Uninstall(ship, count, slot)
371		-- FIXME support failed uninstalls??
372		-- note that failed uninstalls are almost incompatible with Ship::SetShipType
373		assert(uninstalled == count)
374	end
375	self.slots[slot] = {__occupied = 0, __limit = s.__limit}
376	self:__TriggerCallbacks(ship, slot)
377
378end
379
380function EquipSet:Clear(ship, slot_names)
381	if slot_names == nil then
382		for k,_ in pairs(self.slots) do
383			EquipSet__ClearSlot(self, ship, k)
384		end
385
386	elseif type(slot_names) == 'string' then
387		EquipSet__ClearSlot(self, ship, slot_names)
388
389	elseif type(slot_names) == 'table' then
390		for _, s in ipairs(slot_names) do
391			EquipSet__ClearSlot(self, ship, s)
392		end
393	end
394end
395
396function EquipSet:Get(slot, index)
397	if type(index) == "number" then
398		return self.slots[slot][index]
399	end
400	local ret = {}
401	for i,v in pairs(self.slots[slot]) do
402		if type(i) == 'number' then
403			ret[i] = v
404		end
405	end
406	return ret
407end
408
409function EquipSet:Set(ship, slot_name, index, item)
410	local slot = self.slots[slot_name]
411
412	if index < 1 or index > slot.__limit then
413		error("EquipSet:Set(): argument 'index' out of range")
414	end
415
416	local to_remove = slot[index]
417	if item == to_remove then return end
418
419	if not to_remove or to_remove:Uninstall(ship, 1, slot_name) == 1 then
420		if not item or item:Install(ship, 1, slot_name) == 1 then
421			if not item then
422				slot.__occupied = slot.__occupied - 1
423			elseif not to_remove then
424				slot.__occupied = slot.__occupied + 1
425			end
426			slot[index] = item
427			self:__TriggerCallbacks(ship, slot_name)
428		else -- Rollback the uninstall
429			if to_remove then to_remove:Install(ship, 1, slot_name) end
430		end
431	end
432end
433Serializer:RegisterClass("EquipSet", EquipSet)
434return EquipSet
435