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