1--[[ Copyright (c) 2009 Peter "Corsix" Cawley
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of
4this software and associated documentation files (the "Software"), to deal in
5the Software without restriction, including without limitation the rights to
6use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7of the Software, and to permit persons to whom the Software is furnished to do
8so, subject to the following conditions:
9
10The above copyright notice and this permission notice shall be included in all
11copies or substantial portions of the Software.
12
13THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19SOFTWARE. --]]
20
21local TH = require("TH")
22
23--! An `Entity` which occupies at least a single map tile and does not move.
24class "Object" (Entity)
25
26---@type Object
27local Object = _G["Object"]
28
29local orient_mirror = {
30  north = "west",
31  west = "north",
32  east = "south",
33  south = "east",
34}
35
36function Object:getDrawingLayer()
37  return 4
38end
39
40function Object:Object(hospital, object_type, x, y, direction, etc)
41  assert(class.is(hospital, Hospital), "First argument is not a Hospital instance.")
42
43  local th = TH.animation()
44  self:Entity(th)
45
46  if etc == "map object" then
47    if direction % 2 == 0 then
48      direction = "north"
49    else
50      direction = "west"
51    end
52  end
53
54  self.ticks = object_type.ticks
55  self.object_type = object_type
56  self.hospital = hospital
57  self.world = hospital.world
58  self.user = false
59  self.times_used = -1 -- Incremented in the call on the next line
60  self:updateDynamicInfo()
61  self:initOrientation(direction)
62  self:setTile(x, y)
63end
64
65--! Initializes the footprint, finds out what to draw and checks for
66--  split animations.
67--!param direction The orientation in which the object is facing.
68function Object:initOrientation(direction)
69  self.direction = direction
70  local object_type = self.object_type
71  local flags = self.init_anim_flags or 0
72  local anim = object_type.idle_animations[direction]
73  if not anim then
74    anim = object_type.idle_animations[orient_mirror[direction]]
75    flags = 1
76  end
77  local footprint = object_type.orientations
78  footprint = footprint and footprint[direction]
79  if footprint and footprint.early_list then
80    flags = flags + 1024
81  end
82  if footprint and footprint.list_bottom then
83    flags = flags + 2048
84  end
85  local rap = footprint and footprint.render_attach_position
86  if rap and rap[1] and type(rap[1]) == "table" then
87    self.split_anims = {self.th}
88    self.split_anim_positions = rap
89    self.th:setCrop(rap[1].column)
90    for i = 2, #rap do
91      local point = rap[i]
92      local th = TH.animation()
93      th:setCrop(point.column)
94      th:setHitTestResult(self)
95      th:setPosition(Map:WorldToScreen(1-point[1], 1-point[2]))
96      self.split_anims[i] = th
97    end
98  else
99    -- Make sure these variables aren't left behind. The object
100    -- might just have been moved and rotated.
101    self.split_anims = nil
102    self.split_anim_positions = nil
103  end
104  if footprint and footprint.animation_offset then
105    self:setPosition(unpack(footprint.animation_offset))
106  end
107  footprint = footprint and footprint.footprint
108  if footprint then
109    self.footprint = footprint
110  else
111    self.footprint = nil
112  end
113  self:setAnimation(anim, flags)
114end
115
116--! Add methods to a class for creating and controlling a slave object
117function Object.slaveMixinClass(class_method_table)
118  local name = class.name(class_method_table)
119  local super = class.superclass(class_method_table)
120  local super_constructor = super[class.name(super)]
121
122  -- Constructor
123  class_method_table[name] = function(self, hospital, object_type, x, y, direction, ...)
124    super_constructor(self, hospital, object_type, x, y, direction, ...)
125    if object_type.slave_id then
126      local orientation = object_type.orientations
127      orientation = orientation and orientation[direction]
128      if orientation.slave_position then
129        x = x + orientation.slave_position[1]
130        y = y + orientation.slave_position[2]
131      end
132      self.slave = hospital.world:newObject(object_type.slave_id, x, y, direction, ...)
133      self.slave.master = self
134    end
135  end
136
137  -- Slave -> Master redirects
138  local function slave_to_master(method)
139    local super_method = super[method]
140    class_method_table[method] = function(self, ...)
141      local master = self.master
142      if master then
143        return master[method](master, ...)
144      else
145        return super_method(self, ...)
146      end
147    end
148  end
149  slave_to_master("onClick")
150  slave_to_master("updateDynamicInfo")
151  slave_to_master("getDynamicInfo")
152
153  -- Master -> Slave notifications
154  local function master_to_slave(method)
155    local super_method = super[method]
156    class_method_table[method] = function(self, ...)
157      local slave = self.slave
158      if slave then
159        slave[method](slave, ...)
160      end
161      return super_method(self, ...)
162    end
163  end
164  master_to_slave("initOrientation")
165  class_method_table.onDestroy = function(self, ...)
166    local slave = self.slave
167    if slave then
168      self.world:destroyEntity(slave)
169    end
170    return super.onDestroy(self, ...)
171  end
172  class_method_table.setTile = function(self, x, y)
173    if self.slave then
174      local dx, dy = 0, 0
175      local orientation = self.object_type.orientations
176      orientation = orientation and orientation[self.direction]
177      if orientation.slave_position then
178        dx = orientation.slave_position[1]
179        dy = orientation.slave_position[2]
180      end
181      if x then
182        self.slave:setTile(x + dx, y + dy)
183      else
184        self.slave:setTile(x, y)
185      end
186    end
187    return super.setTile(self, x, y)
188  end
189
190  return slave_to_master, master_to_slave
191end
192
193function Object:tick()
194  if self.split_anims then
195    if self.num_animation_ticks then
196      for i = 2, #self.split_anims do
197        local th = self.split_anims[i]
198        for _ = 1, self.num_animation_ticks do
199          th:tick()
200        end
201      end
202    else
203      for i = 2, #self.split_anims do
204        self.split_anims[i]:tick()
205      end
206    end
207  end
208  return Entity.tick(self)
209end
210
211function Object:setPosition(x, y)
212  if self.split_anims then
213    -- The given position is for the primary tile, so position non-primary
214    -- animations relative to the primary one.
215    local bx, by = unpack(self.split_anim_positions[1])
216    for i, th in ipairs(self.split_anims) do
217      local point = self.split_anim_positions[i]
218      local dx, dy = Map:WorldToScreen(1-point[1]+bx, 1-point[2]+by)
219      th:setPosition(x + dx, y + dy)
220    end
221  else
222    self.th:setPosition(x, y)
223  end
224  return self
225end
226
227function Object:setAnimation(animation, flags)
228  if self.split_anims then
229    flags = (flags or 0) + DrawFlags.Crop
230    if self.permanent_flags then
231      flags = flags + self.permanent_flags
232    end
233    if animation ~= self.animation_idx or flags ~= self.animation_flags then
234      self.animation_idx = animation
235      self.animation_flags = flags
236      local anims = self.world.anims
237      for _, th in ipairs(self.split_anims) do
238        th:setAnimation(anims, animation, flags)
239      end
240    end
241    return self
242  else
243    return Entity.setAnimation(self, animation, flags)
244  end
245end
246
247--! Get the primary tile which the object is attached to for rendering
248--[[! For objects which attach to a single tile for rendering, this method will
249return the X and Y Lua world coordinates of that tile. For objects which split
250their rendering over multiple tiles, one of them is arbitrarily designated as
251the primary tile, and its coordinates are returned.
252]]
253function Object:getRenderAttachTile()
254  local x, y = self.tile_x, self.tile_y
255  local offset = self.object_type.orientations
256  if x and offset then
257    offset = offset[self.direction].render_attach_position
258    if self.split_anims then
259      offset = offset[1]
260    end
261    x = x + offset[1]
262    y = y + offset[2]
263  end
264  return x, y
265end
266
267function Object:updateDynamicInfo()
268  self.times_used = self.times_used + 1
269  local object = self.object_type
270  if object.dynamic_info then
271    self:setDynamicInfo("text", {object.name, "", _S.dynamic_info.object.times_used:format(self.times_used)})
272  end
273end
274
275function Object:getSecondaryUsageTile()
276  local x, y = self.tile_x, self.tile_y
277  local offset = self.object_type.orientations
278  if offset then
279    offset = offset[self.direction].use_position_secondary
280    x = x + offset[1]
281    y = y + offset[2]
282  end
283  return x, y
284end
285
286-- This function returns a list of all "only_passable" tiles belonging to an object.
287-- It must be overridden by objects which do not have a footprint, but walkable tiles (e.g. doors of any kind)
288function Object:getWalkableTiles()
289  local tiles = {}
290  for _, xy in ipairs(self.footprint) do
291    if xy.only_passable then
292      tiles[#tiles + 1] = { self.tile_x + xy[1], self.tile_y + xy[2] }
293    end
294  end
295  return tiles
296end
297
298function Object:setTile(x, y)
299  local function coordinatesAreInFootprint(object_footprint, xpos, ypos)
300    for _, xy in ipairs(object_footprint) do
301      if xy[1] == xpos and xy[2] == ypos then
302        return true
303      end
304    end
305    return false
306  end
307
308  local function isEmpty(table)
309    return next(table) == nil
310  end
311
312  local function getComplementaryPassableFlag(passable_flag)
313    if passable_flag == "travelNorth" or passable_flag == "travelSouth" then
314      return passable_flag == "travelNorth" and "travelSouth" or "travelNorth"
315    else
316      return passable_flag == "travelEast" and "travelWest" or "travelEast"
317    end
318  end
319
320  local function setPassableFlags(passable_flag, xpos, ypos, next_x, next_y, value)
321    local flags1 = {}
322    flags1[passable_flag] = value
323    self.world.map.th:setCellFlags(xpos, ypos, flags1)
324    local flags2 = {}
325    flags2[getComplementaryPassableFlag(passable_flag)] = value
326    self.world.map.th:setCellFlags(next_x, next_y, flags2)
327  end
328
329  local direction_parameters = {
330            north = { x = 0, y = -1, buildable_flag = "buildableNorth", passable_flag = "travelNorth", needed_side = "need_north_side"},
331            east = { x = 1, y = 0, buildable_flag =  "buildableEast", passable_flag = "travelEast", needed_side = "need_east_side"},
332            south = { x = 0, y = 1, buildable_flag = "buildableSouth", passable_flag = "travelSouth", needed_side = "need_south_side"},
333            west = { x = -1, y = 0, buildable_flag = "buildableWest", passable_flag = "travelWest", needed_side = "need_west_side"}
334            }
335
336  local direction = self.direction
337  if self.object_type.thob == 50 and direction == "east" then
338    direction = "west"
339  end
340
341  if self.tile_x ~= nil then
342    self.world:removeObjectFromTile(self, self.tile_x, self.tile_y)
343    if self.footprint then
344      local map = self.world.map.th
345      for _, xy in ipairs(self.footprint) do
346        local xpos, ypos = self.tile_x + xy[1], self.tile_y + xy[2]
347
348        if xy.only_side then
349          if self.set_passable_flags then
350            self.set_passable_flags = nil
351            local par = direction_parameters[direction]
352            local passableFlag, next_tile_x, next_tile_y = par.passable_flag, xpos + par.x, ypos + par.y
353            setPassableFlags(passableFlag, xpos, ypos, next_tile_x, next_tile_y, true)
354          end
355          local flags_to_set= {}
356          flags_to_set[direction_parameters[direction].buildable_flag] = true
357          map:setCellFlags(xpos, ypos, flags_to_set)
358        else
359          local flags_to_set = {}
360          for _, value in pairs(direction_parameters) do
361            if coordinatesAreInFootprint(self.footprint, xy[1] + value.x, xy[2] + value.y) or
362                xy.complete_cell or xy[value.needed_side] then
363              flags_to_set[value.buildable_flag] = true
364            end
365          end
366
367          if not isEmpty(flags_to_set) then
368            map:setCellFlags(xpos, ypos, flags_to_set)
369          end
370          if not map:getCellFlags(xpos, ypos).passable then
371            map:setCellFlags(xpos, ypos, {
372              buildable = true,
373              passable = true,
374            })
375          else
376            -- passable tiles can "belong" to multiple objects, so we have to check that
377            if not self.world:isTilePartOfNearbyObject(xpos, ypos, 10) then
378              -- assumption: no object defines a passable tile further than 10 tiles away from its origin
379              map:setCellFlags(xpos, ypos, {
380                buildable = true,
381              })
382            end
383          end
384        end
385      end
386    end
387  end
388
389  local entity_map = self.world.entity_map
390  -- Remove the reference to the object at it's previous coordinates
391  if entity_map then
392    entity_map:removeEntity(self.tile_x,self.tile_y, self)
393  end
394
395  -- Update the objects coordinates
396  self.tile_x = x
397  self.tile_y = y
398
399  -- Update the entity map for the new position
400  if entity_map then
401    entity_map:addEntity(x,y,self)
402  end
403
404  if x then
405    self.th:setDrawingLayer(self:getDrawingLayer())
406    self.th:setTile(self.world.map.th, self:getRenderAttachTile())
407    self.world:addObjectToTile(self, x, y)
408    if self.footprint then
409      local map = self.world.map.th
410      local optional_found = false
411      local room = self.world:getRoom(x, y)
412      local roomId = room and room.id
413      local next_tile_x, next_tile_y = x,y
414      local passable_flag
415
416      for _, xy in ipairs(self.footprint) do
417        local change_flags = true
418        local flags_to_set = {}
419        local lx = x + xy[1]
420        local ly = y + xy[2]
421        local flags
422
423        if xy.optional then
424          if optional_found then
425            -- An optional tile has been accepted, we don't need anymore such tiles.
426            change_flags = false
427          else
428            flags = map:getCellFlags(lx, ly)
429            local is_object_allowed = true
430            if roomId and flags.roomId ~= roomId then
431              is_object_allowed = false
432            elseif xy.only_passable and not self.world.pathfinder:isReachableFromHospital(lx, ly) then
433              is_object_allowed = false
434            end
435
436            if is_object_allowed then
437              change_flags = true
438              optional_found = true
439            else
440              change_flags = false
441            end
442          end
443        end
444
445        flags = map:getCellFlags(lx, ly)
446        if xy.only_side then
447          local par = direction_parameters[direction]
448          flags_to_set[par["buildable_flag"]] = false
449          passable_flag, next_tile_x, next_tile_y = par["passable_flag"], x + par["x"], y + par["y"]
450        else
451          for _, value in pairs(direction_parameters) do
452            if coordinatesAreInFootprint(self.footprint, xy[1] + value["x"], xy[2] + value["y"]) or
453            xy.complete_cell or xy[value["needed_side"]] then
454              if map:getCellFlags(x, y, flags)[value["buildable_flag"]] == 0 then
455                change_flags = false
456              end
457              flags_to_set[value["buildable_flag"]] = false
458            end
459          end
460        end
461
462        if change_flags then
463          if not xy.only_side then
464            map:setCellFlags(lx, ly, {
465              buildable = false,
466              passable = not not xy.only_passable,
467            })
468          end
469          if not isEmpty(flags_to_set) then
470            map:setCellFlags(lx, ly, flags_to_set)
471          end
472          if xy.only_side then
473            if map:getCellFlags(lx, ly)[passable_flag] == true then
474              self.set_passable_flags = true
475              setPassableFlags(passable_flag, lx, ly, next_tile_x, next_tile_y, false)
476            end
477          end
478        end
479      end
480    end
481    if self.split_anims then
482      local map = self.world.map.th
483      local pos = self.split_anim_positions
484      for i = 2, #self.split_anims do
485        self.split_anims[i]:setTile(map, x + pos[i][1], y + pos[i][2])
486      end
487    end
488  else
489    self.th:setTile(nil)
490    if self.split_anims then
491      for i = 2, #self.split_anims do
492        self.split_anims[i]:setTile(nil)
493      end
494    end
495  end
496  self.world:clearCaches()
497  return self
498end
499
500-- Sets the user of this object to the given user. Note that if multiple_users_allowed
501-- is set to true for this object "user" will always be the last added user to this object.
502-- Please do not modify object.user directly.
503--!param user The user that is about to use this object.
504function Object:setUser(user)
505  assert(user, "setUser: Expected a user, got nil") -- It makes no sense to add a nil value
506  self.user = user
507  if self.object_type.multiple_users_allowed then
508    if not self.user_list then
509      self.user_list = {}
510    end
511    table.insert(self.user_list, user)
512  end
513  self.th:makeInvisible()
514  self:removeReservedUser(user)
515end
516
517-- Removes a user from the set of users if multiple_users_allowed is set to true for this
518-- object. Otherwise it sets "user" to nil. Please don't directly change the variable "user".
519function Object:removeUser(user)
520  if self.object_type.multiple_users_allowed then
521    if not user or not self.user_list then
522      -- No user specified, empty the list; or the list didn't exist
523      self.user_list = {}
524    end
525    local found = false
526    for i, users in ipairs(self.user_list) do
527      if users == user then
528        table.remove(self.user_list, i)
529        found = true
530        break
531      end
532    end
533    if not found then
534      self.world:gameLog("Warning: Could not find a humanoid to remove from the user list")
535    end
536    if #self.user_list == 0 then
537      -- No users left, make the object visible again.
538      self.user = nil
539      self.th:makeVisible()
540    end
541  else
542    self.user = nil
543    self.th:makeVisible()
544  end
545end
546
547-- Adds a reserved user for this object. If multiple_users_allowed is set there can be
548-- many users reserving the object. reserved_for will then be the user who most recently
549-- reserved this object.
550function Object:addReservedUser(user)
551  assert(user, "Expected a user, got nil") -- It makes no sense to add a nil value
552  if self.object_type.multiple_users_allowed then
553    if not self.reserved_for_list then
554      self.reserved_for_list = {}
555    end
556    table.insert(self.reserved_for_list, user)
557  else
558    assert(not self.reserved_for or user ~= self.reserved_for, "Object already reserved for another user")
559  end
560  self.reserved_for = user
561end
562
563-- If multiple_users_allowed is true: Removes the user specified from this object's list of reserved users.
564-- If the argument is nil it is assumed that the list should be emptied.
565-- Note that if there are many humanoids reserving this object reserved_for might still be set after a
566-- call to this function.
567-- Otherwise: sets reserved_for to nil.
568function Object:removeReservedUser(user)
569  if self.object_type.multiple_users_allowed then
570    -- No user specified, delete the whole list; or no list found, make it.
571    if not user or not self.reserved_for_list then
572      self.reserved_for_list = {}
573    end
574    local found = false
575    for i, users in ipairs(self.reserved_for_list) do
576      if users == user then
577        table.remove(self.reserved_for_list, i)
578        found = true
579        break
580      end
581    end
582    if not found then
583      self.world:gameLog("Warning: Could not find a humanoid to remove from the reserved user list")
584    end
585    if #self.reserved_for_list == 0 then
586      -- No users left, set reserved_for to nil.
587      self.reserved_for = nil
588    end
589  else
590    self.reserved_for = nil
591  end
592end
593
594-- Checks whether the object is reserved for the specified user.
595-- If the argument is nil a check for any reserved user is done.
596function Object:isReservedFor(user)
597  if not user then
598    if self.reserved_for_list then
599      return #self.reserved_for_list > 0
600    else
601      return not not self.reserved_for
602    end
603  end
604  if self.reserved_for == user then -- "Normal" use
605    return true
606  end
607  if self.object_type.multiple_users_allowed then
608    if not self.reserved_for_list then
609      self.reserved_for_list = {}
610    end
611    for _, users in ipairs(self.reserved_for_list) do
612      if users == user then
613        return true
614      end
615    end
616  end
617  return false
618end
619
620--[[ Called when the object is clicked
621!param ui (UI) The active ui.
622!param button (string) Which button was clicked.
623!param data (table) If some data should be retained after moving an object it is in this table.
624]]
625function Object:onClick(ui, button, data)
626  local window = ui:getWindow(UIEditRoom)
627  if button == "right" or (button == "left" and window and window.in_pickup_mode) then
628    -- This flag can be used if for example some things should only happen as long as the
629    -- object is not picked up. How lovely when it is so logical. :-)
630    local object_list = {{object = self.object_type, qty = 1, existing_object = self}}
631    local room = self:getRoom()
632    window = window and window.visible and window
633    local direction = self.direction
634    if (not room and window) or
635        (room and not (window and window.room == room) and not self.object_type.corridor_object) or
636        (not room and not self.object_type.corridor_object) then
637      return
638    end
639
640    local fullscreen = ui:getWindow(UIFullscreen)
641    if fullscreen then
642      fullscreen:close()
643    end
644
645    if self.object_type.class == "Plant" or self.object_type.class == "Machine" then
646      local taskType = "watering"
647      if self.object_type.class == "Machine" then
648        taskType = "repairing"
649      end
650      local index = self.hospital:getIndexOfTask(self.tile_x, self.tile_y, taskType)
651      if index ~= -1 then
652        self.hospital:removeHandymanTask(index, taskType)
653      end
654    end
655
656    self.picked_up = true
657    self.world:destroyEntity(self)
658    -- NB: the object has to be destroyed before updating/creating the window,
659    -- or the blueprint will be wrong
660    if not window then
661      window = UIPlaceObjects(ui, object_list, false) -- don't pay for
662      ui:addWindow(window)
663    else
664      window:stopPickupItems()
665      window:addObjects(object_list, false) -- don't pay for
666      window:selectObjectType(self.object_type)
667      window:checkEnableConfirm() -- since we removed an object from the room, the requirements may not be met anymore
668    end
669    window:setOrientation(direction)
670    self.orientation_before = self.direction
671    ui:playSound("pickup.wav")
672  end
673end
674
675function Object:eraseObject()
676  self.world.map.th:eraseObjectTypes(self.tile_x, self.tile_y)
677end
678
679function Object:resetAnimation()
680  self.world.map.th:setCellFlags(self.tile_x, self.tile_y, {thob = self.object_type.thob})
681  self.th:setDrawingLayer(self:getDrawingLayer())
682  self.th:setTile(self.world.map.th, self:getRenderAttachTile())
683end
684
685function Object:onDestroy()
686  local room = self:getRoom()
687  if room then
688    room.objects[self] = nil
689  end
690  if self.user_list then
691    for _, user in ipairs(self.user_list) do
692      user:handleRemovedObject(self)
693    end
694    self.user_list = {}
695  elseif self.user then
696    self.user:handleRemovedObject(self)
697  end
698  self.user = nil
699  if self.reserved_for_list then
700    for _, reserver in ipairs(self.reserved_for_list) do
701      reserver:handleRemovedObject(self)
702    end
703    self.reserved_for_list = {}
704  elseif self.reserved_for then
705    self.reserved_for:handleRemovedObject(self)
706  end
707  self.reserved_for = nil
708
709  Entity.onDestroy(self)
710
711  -- Issue 1105 - rebuild wall travel<dir> and pathfinding on side object removal
712  if self.object_type.class == "SideObject" then
713    self.world.map.th:updatePathfinding()
714    self.world:resetSideObjects()
715  end
716end
717
718function Object:afterLoad(old, new)
719  if old < 52 then
720    self.hospital = self.world:getLocalPlayerHospital()
721  end
722  if old < 57 then
723    if self.footprint and self.direction then
724      local object_type = self.object_type
725      local footprint = object_type.orientations
726      footprint = footprint and footprint[self.direction]
727      if footprint and footprint.animation_offset then
728        self:setPosition(unpack(footprint.animation_offset))
729      end
730      footprint = footprint and footprint.footprint
731      self.footprint = footprint
732      if object_type.class == "SideObject" then
733        local flags = {buildable = true}
734        self.world.map.th:setCellFlags(self.tile_x, self.tile_y, flags)
735      end
736      self:setTile(self.tile_x, self.tile_y)
737    end
738  end
739  return Entity.afterLoad(self, old, new)
740end
741
742local all_pathfind_dirs = {[0] = true, [1] = true, [2] = true, [3] = true}
743
744function Object.processTypeDefinition(object_type)
745  if object_type.id == "extinguisher" or object_type.id == "radiator" or
746      object_type.id == "plant" or object_type.id == "reception_desk" or
747      object_type.id == "bench" then
748    object_type.count_category = object_type.id
749  elseif object_type.id ~= "bin" and not object_type.corridor_object and
750      not object_type.id:find("door") then
751    object_type.count_category = "general"
752  end
753  if object_type.orientations then
754    for _, details in pairs(object_type.orientations) do
755      -- Set default values
756      if not details.animation_offset then
757        details.animation_offset = {0, 0}
758      end
759      if not details.render_attach_position then
760        details.render_attach_position = {0, 0}
761      end
762      -- Set the usage position
763      if details.use_position == "passable" then
764        -- "passable" => the *first* passable tile in the footprint list
765        for _, point in pairs(details.footprint) do
766          if point.only_passable then
767            details.use_position = {point[1], point[2]}
768            break
769          end
770        end
771      elseif not details.use_position then
772        details.use_position = {0, 0}
773      end
774      -- Set handyman repair tile
775      if object_type.default_strength and not details.handyman_position then
776        details.handyman_position = details.use_position
777      end
778      -- Find the nearest solid tile in the footprint to the usage position
779      local use_position = details.use_position
780      local solid_near_use_position
781      local solid_near_use_position_d = 10000
782      for _, point in pairs(details.footprint) do repeat
783        if point.only_passable then
784          break -- continue
785        end
786        local d = (point[1] - use_position[1])^2 + (point[2] - use_position[2])^2
787        if d >= solid_near_use_position_d then
788          break -- continue
789        end
790        solid_near_use_position = point
791        solid_near_use_position_d = d
792      until true end
793      if solid_near_use_position_d ~= 1 then
794        details.pathfind_allowed_dirs = all_pathfind_dirs
795      else
796        if use_position[1] < solid_near_use_position[1] then
797          details.pathfind_allowed_dirs = {[1] = true}
798        elseif use_position[1] > solid_near_use_position[1] then
799          details.pathfind_allowed_dirs = {[3] = true}
800        elseif use_position[2] < solid_near_use_position[2] then
801          details.pathfind_allowed_dirs = {[2] = true}
802        else
803          details.pathfind_allowed_dirs = {[0] = true}
804        end
805      end
806      -- Adjust the footprint to make this tile the origin
807      local solid_points = {}
808      if solid_near_use_position then
809        local x, y = unpack(solid_near_use_position)
810        for _, point in pairs(details.footprint) do
811          point[1] = point[1] - x
812          point[2] = point[2] - y
813          if not point.only_passable then
814            solid_points[point[1] * 100 + point[2]] = point
815          end
816        end
817        for _, key in ipairs({"use_position_secondary",
818                              "finish_use_position",
819                              "finish_use_position_secondary"}) do
820          if details[key] then
821            details[key][1] = details[key][1] - x
822            details[key][2] = details[key][2] - y
823          end
824        end
825        use_position[1] = use_position[1] - x
826        use_position[2] = use_position[2] - y
827        if details.slave_position then
828          details.slave_position[1] = details.slave_position[1] - x
829          details.slave_position[2] = details.slave_position[2] - y
830        end
831        local rx, ry = unpack(details.render_attach_position)
832        if type(rx) == "table" then
833          rx, ry = unpack(details.render_attach_position[1])
834          for _, point in ipairs(details.render_attach_position) do
835            point.column = point[1] - point[2]
836            point[1] = point[1] - x
837            point[2] = point[2] - y
838          end
839        else
840          details.render_attach_position[1] = rx - x
841          details.render_attach_position[2] = ry - y
842        end
843        x, y = Map:WorldToScreen(rx + 1, ry + 1)
844        details.animation_offset[1] = details.animation_offset[1] - x
845        details.animation_offset[2] = details.animation_offset[2] - y
846      end
847      -- Find the region around the solid part of the footprint
848      local adjacent_set = {}
849      local adjacent_list = {}
850      details.adjacent_to_solid_footprint = adjacent_list
851      for _, point in pairs(solid_points) do
852        for _, delta in ipairs({{-1, 0}, {0, -1}, {0, 1}, {1, 0}}) do
853          local x = point[1] + delta[1]
854          local y = point[2] + delta[2]
855          local k2 = x * 100 + y
856          if not solid_points[k2] and not adjacent_set[k2] then
857            adjacent_set[k2] = {x, y}
858            adjacent_list[#adjacent_list + 1] = adjacent_set[k2]
859          end
860        end
861      end
862    end
863  end
864end
865
866--[[ Gets the state of an object
867
868! The state can be later used to set the state of this object. This is
869useful when we would destroy and create a new object that should represent
870the same object.
871
872!return (table) state
873]]
874function Object:getState()
875  return {times_used = self.times_used}
876end
877
878--[[ Sets the state of an object
879
880! This is a complement to a pair function. IT will use the generated state
881table to update it's state.
882!param state (table) table holding the state
883!return (void)
884]]
885function Object:setState(state)
886  if state then
887    self.times_used = state.times_used
888  end
889end
890