mirror of
https://git.minetest.land/VoxeLibre/VoxeLibre.git
synced 2024-11-22 18:41:09 +01:00
f78ad93fd3
Reviewed-on: https://git.minetest.land/MineClone2/MineClone2/pulls/4159 Reviewed-by: Mikita Wiśniewski <rudzik8@protonmail.com> Co-authored-by: Eliy21 <eliy21@noreply.git.minetest.land> Co-committed-by: Eliy21 <eliy21@noreply.git.minetest.land>
566 lines
17 KiB
Lua
566 lines
17 KiB
Lua
local S = minetest.get_translator(minetest.get_current_modname())
|
|
|
|
local boat_visual_size = {x = 1, y = 1, z = 1}
|
|
local paddling_speed = 22
|
|
local boat_y_offset = 0.35
|
|
local boat_y_offset_ground = boat_y_offset + 0.6
|
|
local boat_side_offset = 1.001
|
|
local boat_max_hp = 4
|
|
|
|
local function is_group(pos, group)
|
|
local nn = minetest.get_node(pos).name
|
|
return minetest.get_item_group(nn, group) ~= 0
|
|
end
|
|
|
|
local is_water = flowlib.is_water
|
|
local function is_river_water(p)
|
|
local n = minetest.get_node(p).name
|
|
if n == "mclx_core:river_water_source" or n == "mclx_core:river_water_flowing" then
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function is_ice(pos)
|
|
return is_group(pos, "ice")
|
|
end
|
|
|
|
local function is_fire(pos)
|
|
return is_group(pos, "set_on_fire")
|
|
end
|
|
|
|
local function get_sign(i)
|
|
if i == 0 then
|
|
return 0
|
|
else
|
|
return i / math.abs(i)
|
|
end
|
|
end
|
|
|
|
local function get_velocity(v, yaw, y)
|
|
local x = -math.sin(yaw) * v
|
|
local z = math.cos(yaw) * v
|
|
return {x = x, y = y, z = z}
|
|
end
|
|
|
|
local function get_v(v)
|
|
return math.sqrt(v.x ^ 2 + v.z ^ 2)
|
|
end
|
|
|
|
local function check_object(obj)
|
|
return obj and (obj:is_player() or obj:get_luaentity()) and obj
|
|
end
|
|
|
|
local function get_visual_size(obj)
|
|
return obj:is_player() and {x = 1, y = 1, z = 1} or obj:get_luaentity()._old_visual_size or obj:get_properties().visual_size
|
|
end
|
|
|
|
local function set_attach(boat)
|
|
boat._driver:set_attach(boat.object, "",
|
|
{x = 0, y = 1.5, z = 1}, {x = 0, y = 0, z = 0})
|
|
end
|
|
|
|
local function set_double_attach(boat)
|
|
boat._driver:set_attach(boat.object, "",
|
|
{x = 0, y = 0.42, z = 0.8}, {x = 0, y = 0, z = 0})
|
|
if boat._passenger:is_player() then
|
|
boat._passenger:set_attach(boat.object, "",
|
|
{x = 0, y = 0.42, z = -6.2}, {x = 0, y = 0, z = 0})
|
|
else
|
|
boat._passenger:set_attach(boat.object, "",
|
|
{x = 0, y = 0.42, z = -4.5}, {x = 0, y = 270, z = 0})
|
|
end
|
|
end
|
|
local function set_choat_attach(boat)
|
|
boat._driver:set_attach(boat.object, "",
|
|
{x = 0, y = 1.5, z = 1}, {x = 0, y = 0, z = 0})
|
|
end
|
|
|
|
local function attach_object(self, obj)
|
|
if self._driver and not self._inv_id then
|
|
if self._driver:is_player() then
|
|
self._passenger = obj
|
|
else
|
|
self._passenger = self._driver
|
|
self._driver = obj
|
|
end
|
|
set_double_attach(self)
|
|
else
|
|
self._driver = obj
|
|
if self._inv_id then
|
|
set_choat_attach(self)
|
|
else
|
|
set_attach(self)
|
|
end
|
|
end
|
|
|
|
local visual_size = get_visual_size(obj)
|
|
local yaw = self.object:get_yaw()
|
|
obj:set_properties({visual_size = vector.divide(visual_size, boat_visual_size)})
|
|
|
|
if obj:is_player() then
|
|
local name = obj:get_player_name()
|
|
mcl_player.player_attached[name] = true
|
|
minetest.after(0.2, function(name)
|
|
local player = minetest.get_player_by_name(name)
|
|
if player then
|
|
mcl_player.player_set_animation(player, "sit" , 30)
|
|
end
|
|
end, name)
|
|
obj:set_look_horizontal(yaw)
|
|
mcl_title.set(obj, "actionbar", {text=S("Sneak to dismount"), color="white", stay=60})
|
|
else
|
|
obj:get_luaentity()._old_visual_size = visual_size
|
|
end
|
|
end
|
|
|
|
local function detach_object(obj, change_pos)
|
|
if not obj or not obj:get_pos() then return end
|
|
obj:set_detach()
|
|
obj:set_properties({visual_size = get_visual_size(obj)})
|
|
if obj:is_player() then
|
|
mcl_player.player_attached[obj:get_player_name()] = false
|
|
mcl_player.player_set_animation(obj, "stand" , 30)
|
|
else
|
|
obj:get_luaentity()._old_visual_size = nil
|
|
end
|
|
if change_pos then
|
|
obj:set_pos(vector.add(obj:get_pos(), vector.new(0, 0.2, 0)))
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Boat entity
|
|
--
|
|
|
|
local boat = {
|
|
physical = true,
|
|
pointable = true,
|
|
-- Warning: Do not change the position of the collisionbox top surface,
|
|
-- lowering it causes the boat to fall through the world if underwater
|
|
collisionbox = {-0.5, -0.15, -0.5, 0.5, 0.55, 0.5},
|
|
selectionbox = {-0.7, -0.15, -0.7, 0.7, 0.55, 0.7},
|
|
visual = "mesh",
|
|
mesh = "mcl_boats_boat.b3d",
|
|
textures = { "mcl_boats_texture_oak_boat.png", "blank.png" },
|
|
visual_size = boat_visual_size,
|
|
hp_max = boat_max_hp,
|
|
damage_texture_modifier = "^[colorize:white:0",
|
|
|
|
_driver = nil, -- Attached driver (player) or nil if none
|
|
_passenger = nil,
|
|
_v = 0, -- Speed
|
|
_last_v = 0, -- Temporary speed variable
|
|
_removed = false, -- If true, boat entity is considered removed (e.g. after punch) and should be ignored
|
|
_itemstring = "mcl_boats:boat", -- Itemstring of the boat item (implies boat type)
|
|
_animation = 0, -- 0: not animated; 1: paddling forwards; -1: paddling backwards
|
|
_regen_timer = 0,
|
|
_damage_anim = 0,
|
|
}
|
|
|
|
minetest.register_on_respawnplayer(detach_object)
|
|
|
|
function boat.on_rightclick(self, clicker)
|
|
if self._passenger or not clicker or clicker:get_attach() or (self.name == "mcl_boats:chest_boat" and self._driver) then
|
|
return
|
|
end
|
|
attach_object(self, clicker)
|
|
end
|
|
|
|
|
|
function boat.on_activate(self, staticdata, dtime_s)
|
|
self.object:set_armor_groups({fleshy = 125})
|
|
local data = minetest.deserialize(staticdata)
|
|
if type(data) == "table" then
|
|
self._v = data.v
|
|
self._last_v = self._v
|
|
self._itemstring = data.itemstring
|
|
|
|
-- Update the texutes for existing old boat entity instances.
|
|
-- Maybe remove this in the future.
|
|
if #data.textures ~= 2 then
|
|
local has_chest = self._itemstring:find("chest")
|
|
data.textures = {
|
|
data.textures[1]:gsub("_chest", ""),
|
|
has_chest and "mcl_chests_normal.png" or "blank.png"
|
|
}
|
|
end
|
|
|
|
self.object:set_properties({textures = data.textures})
|
|
end
|
|
end
|
|
|
|
function boat.get_staticdata(self)
|
|
return minetest.serialize({
|
|
v = self._v,
|
|
itemstring = self._itemstring,
|
|
textures = self.object:get_properties().textures
|
|
})
|
|
end
|
|
|
|
function boat.on_death(self, killer)
|
|
mcl_burning.extinguish(self.object)
|
|
|
|
if killer and killer:is_player() and minetest.is_creative_enabled(killer:get_player_name()) then
|
|
local inv = killer:get_inventory()
|
|
if not inv:contains_item("main", self._itemstring) then
|
|
inv:add_item("main", self._itemstring)
|
|
end
|
|
else
|
|
minetest.add_item(self.object:get_pos(), self._itemstring)
|
|
end
|
|
if self._driver then
|
|
detach_object(self._driver)
|
|
end
|
|
if self._passenger then
|
|
detach_object(self._passenger)
|
|
end
|
|
self._driver = nil
|
|
self._passenger = nil
|
|
end
|
|
|
|
function boat.on_punch(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
|
|
if damage > 0 then
|
|
self._regen_timer = 0
|
|
end
|
|
end
|
|
|
|
function boat.on_step(self, dtime, moveresult)
|
|
mcl_burning.tick(self.object, dtime, self)
|
|
-- mcl_burning.tick may remove object immediately
|
|
if not self.object:get_pos() then return end
|
|
|
|
self._v = get_v(self.object:get_velocity()) * get_sign(self._v)
|
|
local v_factor = 1
|
|
local v_slowdown = 0.02
|
|
local p = self.object:get_pos()
|
|
local on_water = true
|
|
local on_ice = false
|
|
local in_water = is_water({x=p.x, y=p.y-boat_y_offset+1, z=p.z})
|
|
local in_river_water = is_river_water({x=p.x, y=p.y-boat_y_offset+1, z=p.z})
|
|
local waterp = {x=p.x, y=p.y-boat_y_offset - 0.1, z=p.z}
|
|
if not is_water(waterp) then
|
|
on_water = false
|
|
if not in_water and is_ice(waterp) then
|
|
on_ice = true
|
|
elseif is_fire({x=p.x, y=p.y-boat_y_offset, z=p.z}) then
|
|
boat.on_death(self, nil)
|
|
self.object:remove()
|
|
return
|
|
else
|
|
v_slowdown = 0.04
|
|
v_factor = 0.5
|
|
end
|
|
elseif in_water and not in_river_water then
|
|
on_water = false
|
|
in_water = true
|
|
v_factor = 0.75
|
|
v_slowdown = 0.05
|
|
end
|
|
|
|
local hp = self.object:get_hp()
|
|
local regen_timer = self._regen_timer + dtime
|
|
if hp >= boat_max_hp then
|
|
regen_timer = 0
|
|
elseif regen_timer >= 0.5 then
|
|
hp = hp + 1
|
|
self.object:set_hp(hp)
|
|
regen_timer = 0
|
|
end
|
|
self._regen_timer = regen_timer
|
|
|
|
if moveresult and moveresult.collides then
|
|
for _, collision in pairs(moveresult.collisions) do
|
|
local pos = collision.node_pos
|
|
if collision.type == "node" and minetest.get_item_group(minetest.get_node(pos).name, "dig_by_boat") > 0 then
|
|
minetest.dig_node(pos)
|
|
end
|
|
end
|
|
end
|
|
|
|
local had_passenger = self._passenger
|
|
|
|
self._driver = check_object(self._driver)
|
|
self._passenger = check_object(self._passenger)
|
|
|
|
if self._passenger then
|
|
if not self._driver then
|
|
self._driver = self._passenger
|
|
self._passenger = nil
|
|
else
|
|
local ctrl = self._passenger:get_player_control()
|
|
if ctrl and ctrl.sneak then
|
|
detach_object(self._passenger, true)
|
|
self._passenger = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
if self._driver then
|
|
if had_passenger and not self._passenger then
|
|
set_attach(self)
|
|
end
|
|
local ctrl = self._driver:get_player_control()
|
|
if ctrl and ctrl.sneak then
|
|
detach_object(self._driver, true)
|
|
self._driver = nil
|
|
return
|
|
end
|
|
local yaw = self.object:get_yaw()
|
|
if ctrl and ctrl.up then
|
|
-- Forwards
|
|
self._v = self._v + 0.1 * v_factor
|
|
|
|
-- Paddling animation
|
|
if self._animation ~= 1 then
|
|
self.object:set_animation({x=0, y=40}, paddling_speed, 0, true)
|
|
self._animation = 1
|
|
end
|
|
elseif ctrl and ctrl.down then
|
|
-- Backwards
|
|
self._v = self._v - 0.1 * v_factor
|
|
|
|
-- Paddling animation, reversed
|
|
if self._animation ~= -1 then
|
|
self.object:set_animation({x=0, y=40}, -paddling_speed, 0, true)
|
|
self._animation = -1
|
|
end
|
|
else
|
|
-- Stop paddling animation if no control pressed
|
|
if self._animation ~= 0 then
|
|
self.object:set_animation({x=0, y=40}, 0, 0, true)
|
|
self._animation = 0
|
|
end
|
|
end
|
|
if ctrl and ctrl.left then
|
|
if self._v < 0 then
|
|
self.object:set_yaw(yaw - (1 + dtime) * 0.03 * v_factor)
|
|
else
|
|
self.object:set_yaw(yaw + (1 + dtime) * 0.03 * v_factor)
|
|
end
|
|
elseif ctrl and ctrl.right then
|
|
if self._v < 0 then
|
|
self.object:set_yaw(yaw + (1 + dtime) * 0.03 * v_factor)
|
|
else
|
|
self.object:set_yaw(yaw - (1 + dtime) * 0.03 * v_factor)
|
|
end
|
|
end
|
|
else
|
|
-- Stop paddling without driver
|
|
if self._animation ~= 0 then
|
|
self.object:set_animation({x=0, y=40}, 0, 0, true)
|
|
self._animation = 0
|
|
end
|
|
|
|
for _, obj in pairs(minetest.get_objects_inside_radius(self.object:get_pos(), 1.3)) do
|
|
local entity = obj:get_luaentity()
|
|
if entity and entity.is_mob then
|
|
attach_object(self, obj)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
local s = get_sign(self._v)
|
|
if not on_ice and not on_water and not in_water and math.abs(self._v) > 2.0 then
|
|
v_slowdown = math.min(math.abs(self._v) - 2.0, v_slowdown * 5)
|
|
elseif not on_ice and in_water and math.abs(self._v) > 1.5 then
|
|
v_slowdown = math.min(math.abs(self._v) - 1.5, v_slowdown * 5)
|
|
end
|
|
self._v = self._v - v_slowdown * s
|
|
if s ~= get_sign(self._v) then
|
|
self._v = 0
|
|
end
|
|
|
|
p.y = p.y - boat_y_offset
|
|
local new_velo
|
|
local new_acce
|
|
if not is_water(p) and not on_ice then
|
|
-- Not on water or inside water: Free fall
|
|
--local nodedef = minetest.registered_nodes[minetest.get_node(p).name]
|
|
new_acce = {x = 0, y = -9.8, z = 0}
|
|
new_velo = get_velocity(self._v, self.object:get_yaw(),
|
|
self.object:get_velocity().y)
|
|
else
|
|
p.y = p.y + 1
|
|
local is_obsidian_boat = self.object:get_luaentity()._itemstring == "mcl_boats:boat_obsidian"
|
|
if is_river_water(p) then
|
|
local y = self.object:get_velocity().y
|
|
if y >= 5 then
|
|
y = 5
|
|
elseif y < 0 then
|
|
new_acce = {x = 0, y = 10, z = 0}
|
|
else
|
|
new_acce = {x = 0, y = 2, z = 0}
|
|
end
|
|
new_velo = get_velocity(self._v, self.object:get_yaw(), y)
|
|
self.object:set_pos(self.object:get_pos())
|
|
elseif is_water(p) and not is_river_water(p) or is_obsidian_boat then
|
|
-- Inside water: Slowly sink
|
|
local y = self.object:get_velocity().y
|
|
y = y - 0.01
|
|
if y < -0.2 then
|
|
y = -0.2
|
|
end
|
|
new_acce = {x = 0, y = 0, z = 0}
|
|
new_velo = get_velocity(self._v, self.object:get_yaw(), y)
|
|
else
|
|
-- On top of water
|
|
new_acce = {x = 0, y = 0, z = 0}
|
|
if math.abs(self.object:get_velocity().y) < 0 then
|
|
new_velo = get_velocity(self._v, self.object:get_yaw(), 0)
|
|
else
|
|
new_velo = get_velocity(self._v, self.object:get_yaw(),
|
|
self.object:get_velocity().y)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Terminal velocity: 8 m/s per axis of travel
|
|
local terminal_velocity = on_ice and 57.1 or 8.0
|
|
for _,axis in pairs({"z","y","x"}) do
|
|
if math.abs(new_velo[axis]) > terminal_velocity then
|
|
new_velo[axis] = terminal_velocity * get_sign(new_velo[axis])
|
|
end
|
|
end
|
|
|
|
local yaw = self.object:get_yaw()
|
|
local anim = (boat_max_hp - hp - regen_timer * 2) / boat_max_hp * math.pi / 4
|
|
|
|
self.object:set_rotation(vector.new(anim, yaw, anim))
|
|
self.object:set_velocity(new_velo)
|
|
self.object:set_acceleration(new_acce)
|
|
end
|
|
|
|
-- Register one entity for all boat types
|
|
minetest.register_entity("mcl_boats:boat", boat)
|
|
|
|
local cboat = table.copy(boat)
|
|
cboat.textures = { "mcl_boats_texture_oak_chest_boat.png", "mcl_chests_normal.png" }
|
|
cboat._itemstring = "mcl_boats:chest_boat"
|
|
cboat.collisionbox = {-0.5, -0.15, -0.5, 0.5, 0.75, 0.5}
|
|
cboat.selectionbox = {-0.7, -0.15, -0.7, 0.7, 0.75, 0.7}
|
|
|
|
minetest.register_entity("mcl_boats:chest_boat", cboat)
|
|
mcl_entity_invs.register_inv("mcl_boats:chest_boat","Boat",27)
|
|
|
|
local boat_ids = { "boat", "boat_spruce", "boat_birch", "boat_jungle", "boat_acacia", "boat_dark_oak", "boat_obsidian", "boat_mangrove", "boat_cherry", "chest_boat", "chest_boat_spruce", "chest_boat_birch", "chest_boat_jungle", "chest_boat_acacia", "chest_boat_dark_oak", "chest_boat_mangrove", "chest_boat_cherry" }
|
|
local names = { S("Oak Boat"), S("Spruce Boat"), S("Birch Boat"), S("Jungle Boat"), S("Acacia Boat"), S("Dark Oak Boat"), S("Obsidian Boat"), S("Mangrove Boat"), S("Cherry Boat"), S("Oak Chest Boat"), S("Spruce Chest Boat"), S("Birch Chest Boat"), S("Jungle Chest Boat"), S("Acacia Chest Boat"), S("Dark Oak Chest Boat"), S("Mangrove Chest Boat"), S("Cherry Chest Boat") }
|
|
local craftstuffs = { "mcl_core:wood", "mcl_core:sprucewood", "mcl_core:birchwood", "mcl_core:junglewood", "mcl_core:acaciawood", "mcl_core:darkwood", "mcl_core:obsidian", "mcl_mangrove:mangrove_wood", "mcl_cherry_blossom:cherrywood" }
|
|
|
|
for b=1, #boat_ids do
|
|
local itemstring = "mcl_boats:"..boat_ids[b]
|
|
|
|
local longdesc, usagehelp, tt_help, help, helpname
|
|
help = false
|
|
-- Only create one help entry for all boats
|
|
if b == 1 then
|
|
help = true
|
|
longdesc = S("Boats are used to travel on the surface of water.")
|
|
usagehelp = S("Rightclick on a water source to place the boat. Rightclick the boat to enter it. Use [Left] and [Right] to steer, [Forwards] to speed up and [Backwards] to slow down or move backwards. Use [Sneak] to leave the boat, punch the boat to make it drop as an item.")
|
|
helpname = S("Boat")
|
|
end
|
|
tt_help = S("Water vehicle")
|
|
|
|
local inventory_image
|
|
local texture
|
|
local id = boat_ids[b]
|
|
if id:find("chest") then
|
|
if id == "chest_boat" then id = "oak" end
|
|
local id = id:gsub("chest_boat_", "")
|
|
inventory_image = "mcl_boats_" .. id .. "_chest_boat.png"
|
|
texture = "mcl_boats_texture_" .. id .. "_boat.png"
|
|
else
|
|
if id == "boat" then id = "oak" end
|
|
local id = id:gsub("boat_", "")
|
|
inventory_image = "mcl_boats_" .. id .. "_boat.png"
|
|
texture = "mcl_boats_texture_" .. id .. "_boat.png"
|
|
end
|
|
|
|
minetest.register_craftitem(itemstring, {
|
|
description = names[b],
|
|
_tt_help = tt_help,
|
|
_doc_items_create_entry = help,
|
|
_doc_items_entry_name = helpname,
|
|
_doc_items_longdesc = longdesc,
|
|
_doc_items_usagehelp = usagehelp,
|
|
inventory_image = inventory_image,
|
|
liquids_pointable = true,
|
|
groups = { boat = 1, transport = 1},
|
|
stack_max = 1,
|
|
on_place = function(itemstack, placer, pointed_thing)
|
|
if pointed_thing.type ~= "node" then
|
|
return itemstack
|
|
end
|
|
|
|
-- Call on_rightclick if the pointed node defines it
|
|
local node = minetest.get_node(pointed_thing.under)
|
|
if placer and not placer:get_player_control().sneak then
|
|
if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
|
|
return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
|
|
end
|
|
end
|
|
|
|
local pos = table.copy(pointed_thing.under)
|
|
local dir = vector.subtract(pointed_thing.above, pointed_thing.under)
|
|
|
|
if math.abs(dir.x) > 0.9 or math.abs(dir.z) > 0.9 then
|
|
pos = vector.add(pos, vector.multiply(dir, boat_side_offset))
|
|
elseif is_water(pos) then
|
|
pos = vector.add(pos, vector.multiply(dir, boat_y_offset))
|
|
else
|
|
pos = vector.add(pos, vector.multiply(dir, boat_y_offset_ground))
|
|
end
|
|
local boat_ent = "mcl_boats:boat"
|
|
local chest_tex = "blank.png"
|
|
if itemstring:find("chest") then
|
|
boat_ent = "mcl_boats:chest_boat"
|
|
chest_tex = "mcl_chests_normal.png"
|
|
end
|
|
local boat = minetest.add_entity(pos, boat_ent)
|
|
boat:get_luaentity()._itemstring = itemstring
|
|
boat:set_properties({ textures = { texture, chest_tex } })
|
|
boat:set_yaw(placer:get_look_horizontal())
|
|
if not minetest.is_creative_enabled(placer:get_player_name()) then
|
|
itemstack:take_item()
|
|
end
|
|
return itemstack
|
|
end,
|
|
_on_dispense = function(stack, pos, droppos, dropnode, dropdir)
|
|
local below = {x=droppos.x, y=droppos.y-1, z=droppos.z}
|
|
local belownode = minetest.get_node(below)
|
|
-- Place boat as entity on or in water
|
|
if minetest.get_item_group(dropnode.name, "water") ~= 0 or (dropnode.name == "air" and minetest.get_item_group(belownode.name, "water") ~= 0) then
|
|
minetest.add_entity(droppos, "mcl_boats:boat")
|
|
else
|
|
minetest.add_item(droppos, stack)
|
|
end
|
|
end,
|
|
})
|
|
|
|
local c = craftstuffs[b]
|
|
if not itemstring:find("chest") then
|
|
minetest.register_craft({
|
|
output = itemstring:gsub(":boat",":chest_boat"),
|
|
recipe = {
|
|
{"mcl_chests:chest"},
|
|
{itemstring},
|
|
},
|
|
})
|
|
minetest.register_craft({
|
|
output = itemstring,
|
|
recipe = {
|
|
{c, "", c},
|
|
{c, c, c},
|
|
},
|
|
})
|
|
end
|
|
end
|
|
|
|
minetest.register_craft({
|
|
type = "fuel",
|
|
recipe = "group:boat",
|
|
burntime = 20,
|
|
})
|
|
|
|
if minetest.get_modpath("doc_identifier") then
|
|
doc.sub.identifier.register_object("mcl_boats:boat", "craftitems", "mcl_boats:boat")
|
|
end
|