IMIsle Modkit
Pages in this area

Garage

The garage lets players store a dinosaur and take it out again later. The server plugin does the saving, the removal of the parked dinosaur and the restore. Your resource adds the commands and the rules. All functions are in modkit.garage.

Read how park and restore work first. Two facts matter for your rules. A restore needs a living dinosaur of the same species, so the player spawns a fresh one and the saved state is written onto it. And a slot is only used up once the state was written.

A complete resource

resources/garage/manifest.lua

name "garage"
server "server.lua"

resources/garage/server.lua

local COOLDOWN = 15 * 60
local MIN_GROWTH = 0.3
local MIN_HEALTH = 0.7
local SAFE_DISTANCE = 15000

modkit.garage.setLimit(3)
modkit.garage.setLimitCallback(function(player)
  return player:hasPermission("garage.vip") and 8 or 3
end)

local function cooldownLeft(player)
  local last = modkit.storage.get("restore:" .. player.steamId, 0)
  return math.max(0, last + COOLDOWN - os.time())
end

local function enemyNearby(player)
  for _, other in ipairs(modkit.players.list()) do
    if other.steamId ~= player.steamId and other.hasDino and other.alive
       and not player:inSameGroup(other) then
      local distance = player:distanceTo(other)
      if distance and distance < SAFE_DISTANCE then return true end
    end
  end
  return false
end

modkit.events.add("garageParking", function(player)
  if player.growth < MIN_GROWTH then
    player:message("You need 30% growth to park.")
    return false
  end
  if player.health < player.maxHealth * MIN_HEALTH then
    player:message("You need 70% health to park.")
    return false
  end
  if enemyNearby(player) then
    player:message("Another player is within 150 m.")
    return false
  end
end)

modkit.events.add("garageRestoring", function(player, slot)
  local wait = cooldownLeft(player)
  if wait > 0 then
    player:message("Next restore in " .. modkit.util.formatTime(wait))
    return false
  end
  if modkit.population.isBlocked(slot.species) then
    player:message(slot.species .. " is full right now.")
    return false
  end
  if enemyNearby(player) then
    player:message("Another player is within 150 m.")
    return false
  end
end)

modkit.events.add("garageRestored", function(player, slot)
  modkit.storage.set("restore:" .. player.steamId, os.time())
  player:notify(slot.name .. " is back.")
end)

modkit.events.add("garageRestoreFailed", function(steamId, slotId, reason)
  modkit.log.warn("restore of slot " .. slotId .. " for " .. steamId .. " ended early: " .. reason)
end)

modkit.commands.add("park", function(player, ...)
  local name = table.concat({ ... }, " ")
  local slot, why = modkit.garage.park(player, { name = name ~= "" and name or nil })
  if slot then
    player:message("Parked as slot " .. slot.id .. ". Spawn a new " .. slot.species .. " and use /restore " .. slot.id)
  else
    player:message("Not parked: " .. why)
  end
end)

modkit.commands.add("garage", function(player)
  local slots = modkit.garage.list(player)
  if #slots == 0 then player:message("Your garage is empty.") return end
  for _, slot in ipairs(slots) do
    player:message(string.format("%d: %s, %s, %d%%", slot.id, slot.name, slot.species, math.floor(slot.growth * 100)))
  end
  player:message(#slots .. " of " .. modkit.garage.getLimit(player) .. " slots used")
end)

modkit.commands.add("restore", function(player, slotId)
  local id = tonumber(slotId)
  if not id then player:message("usage: /restore <slot>") return end
  local slot, why = modkit.garage.restore(player, id, { atParkPosition = false })
  player:message(slot and ("Restoring " .. slot.name .. " ...") or ("Not restored: " .. why))
end)

modkit.commands.add("givedino", function(admin, steamId, species, growth)
  if not steamId or not species then admin:message("usage: /givedino <steamId> <species> [growth]") return end
  local slot, why = modkit.garage.give(steamId, { species = species, growth = tonumber(growth) or 1, fill = true }, { name = "Gift " .. species })
  admin:message(slot and ("Slot " .. slot.id .. " created") or why)
end, "garage.give")

Add load garage to server.cfg and restart.

Notes on the rules

The checks sit in the two "before" events and not in the commands. That way they also hold when another resource calls modkit.garage.park for the player.

garageRestoring fires after the plugin checked the slot, the species and that no other restore runs. If your handler lets it through, the restore almost always starts, so the cooldown is written in garageRestored.

The cooldown is kept in modkit.storage, so it survives a restart.

enemyNearby uses snapshot positions, which can be half a second old. For a 150 m rule that does not matter.

atParkPosition = true would put the dinosaur back where it was parked. Most servers leave it off, because it lets players teleport across the map through the garage.

What players will notice

  • Parking kills the dinosaur. The player sees the death screen and picks a new character. The game has no clean way to take a dinosaur away from a player, so the plugin uses the death.
  • To restore, the player spawns as the same species first and then uses /restore. The fresh dinosaur grows to the saved size within a few seconds.
  • The kill feed and playerDied fire for a parked dinosaur. If you count deaths, skip a death that follows a garageParked of the same player within a few seconds.

Where the data is

data\garage\<steamId>.json under the server root. One file per player with all slots. You can delete a file to empty a garage while the server is stopped. The format of state is the one of player:getState().