guns4d-cd2025/classes/Player_model_handler.lua

297 lines
14 KiB
Lua

--- player_model_handler
--
-- ## defining the player model when holding a gun
--
-- each player model should have a "gun holding equivelant". There are numerous reasons for this
-- first and foremost is that because Minetest is a [redacted mindless insults].
-- because of this you cannot unset bone offsets and return to normal animations.
-- Bone offsets are needed for the arms to aim at the gun there's no simple way around this fact.
-- Since every model is different custom behavior has to be defined for most.
--
-- @class player_model_handler
-- @compact
--- player_model_handler fields
-- @table fields
-- @field offsets @{fields.offsets}
Guns4d.player_model_handler = {
handlers = {}, --not for children, this stores a global list of handlers by meshname.
-- @table fields.offsets
offsets = {
-- a list of offsets relative to the whole model
global = {
--right arm (for hipfire bone)
},
-- a list of offsets relative to their parents (at rest position)
relative = { --none of these are specifically needed...
--left arm
--right arm
--head
},
},
--model generation attributes
override_bones = { --a list of bones to be read and or generated
__overfill = true,
Arm_Right = "guns4d_arm_right",
Arm_Left = "guns4d_arm_left",
Head = "guns4d_head"
},
new_bones = { --currently only supports empty bones. Sets at identity rotation, position 0, and parentless
"guns4d_gun_bone",
"guns4d_hipfire_bone"
},
bone_aliases = { --names of bones used by the model handler and other parts of guns4d.
__overfill = true,
arm_right = "guns4d_arm_right",
arm_left = "guns4d_arm_left",
head = "guns4d_head",
gun = "guns4d_gun_bone",
},
still_frame = 0, --the frame to take bone offsets from. This system has to be improved in the future (to allow better animation support)- it works for now though.
auto_generate = true,
scale = 1, --this is important for autogen
output_path = minetest.get_modpath("guns4d").."/temp/",
compatible_meshes = { --list of meshes and their corresponding partner meshes for this handler. Must have the same bones used by guns4d.
--["character.b3d"] = "guns4d_character.b3d", this would tell the handler to use guns4d_character.b3d instead of generating a new one based on the override parameters.
["character.b3d"] = true, --it is compatible but it has no predefined model, one will be therefore generated using the override_bone_aliases parameters.
__overfill = true
},
gun_bone_location = vector.new(),
fallback_mesh = "character.b3d", --if no meshes are found in "compatible_meshes" it chooses this index in "compatible_meshes"
is_new_default = true --this will set the this to be the default handler.
}
local player_model = Guns4d.player_model_handler
function player_model.set_default_handler(class_or_name)
assert(class_or_name, "class or mesh name (string) needed. Example: 'character.b3d' sets the default handler to whatever handler is used for character.b3d.")
local handler = assert(((type(class_or_name) == "class") and class_or_name) or player_model.get_handler(class_or_name), "no handler by the name '"..tostring(class_or_name).."' found.")
assert(not handler.instance, "cannot set instance of a handler as the default player_model_handler")
player_model.default_handler = handler
end
function player_model.get_handler(meshname)
local selected_handler = player_model.handlers[meshname] or player_model.main
if selected_handler then return selected_handler end
return player_model.default_handler
end
function player_model:add_compatible_mesh(original, replacement)
assert(not self.instance, "attempt to call class method on an object. Cannot modify original class from an instance.")
assert(original and replacement, "one or more parameters missing")
self.compatible_meshes[original] = replacement
player_model.handlers[original] = self
end
--we store the read file so it can be reused in the constructor if needed.
local model_buffer
local modpath = minetest.get_modpath("guns4d")
function player_model:custom_b3d_generation_parameters(b3d)
--empty for now, this is for model customizations.
return b3d
end
function player_model:generate_b3d_model(name)
assert(self and name, "error while generating a b3d model. Name not provided or not called as a method.")
if not mtul.paths.media_paths[name.b3d] then
--generate a new model
local filename = string.sub(name, 1, -5).."_guns4d_temp.b3d"
local new_path = self.output_path..filename
--buffer and modify the model
model_buffer = mtul.b3d_reader.read_model(name)
local b3d = model_buffer
local replaced = {}
--add bone... forgot i made this so simple by adding node_paths
for _, node in pairs(b3d.node_paths) do
if self.override_bones[node.name] then
replaced[node.name] = true
--change the name
node.name = self.override_bones[node.name]
--unset rotation because it breaks shit
local rot = node.rotation
for i, v in pairs(node.keys) do
v.rotation = rot
end
--node.rotation = {0,0,0,1}
end
end
--check bones were replaced to avoid errors.
for i, v in pairs(self.override_bones) do
if (not replaced[i]) and i~="__overfill" then
error("bone '"..i.."' not replaced with it's guns4d counterpart, bone was not found. Check bone name")
end
end
for i, v in pairs(self.new_bones) do
table.insert(b3d.node.children, {
name = v,
position = {0,0,0},
scale = {1/self.scale,1/self.scale,1/self.scale},
rotation = {0,0,0,1},
children = {},
bone = {} --empty works?
})
end
b3d=self:custom_b3d_generation_parameters(b3d)
--write temp model
local writefile = io.open(new_path, "w+b")
mtul.b3d_writer.write_model_to_file(b3d, writefile)
writefile:close()
--send to player media paths
minetest.after(0, function()
assert(
minetest.dynamic_add_media({filepath = new_path}, function()end),
"failed sending media"
)
end)
mtul.paths.media_paths[filename] = new_path
mtul.paths.modname_by_media[filename] = "guns4d"
return filename
end
end
-- main update function
function player_model:update(dt)
--assert(dt, "delta time (dt) not provided.")
--assert(self.instance, "attempt to call object method on a class")
self:update_aiming(dt)
self:update_head(dt)
self:update_arm_bones(dt)
end
function player_model:update_aiming(dt)
--gun bones:
local player = self.player
local handler = self.handler
local gun = handler.gun
local pprops = handler:get_properties()
local vs = pprops.visual_size
local player_trans = gun.total_offsets.player_trans --player translation.
local hip_pos = self.offsets.global.arm_right
local ip = Guns4d.math.smooth_ratio(handler.control_handler.ads_location or 0)
local ip_inv = 1-ip
local pos = self.gun_bone_location --reuse allocated table
--interpolate between the eye and arm pos
pos.x = ((hip_pos.x*10*ip_inv) + (player_trans.x*10/vs.y)) + ((gun and gun.properties.ads.horizontal_offset*10*ip/vs.y) or 0 )
pos.y = ((hip_pos.y*10*ip_inv) + (player_trans.y*10/vs.y)) + (pprops.eye_height*10*ip/vs.y)
pos.z = ((hip_pos.z*10*ip_inv) + (player_trans.z*10/vs.y))
local dir = vector.rotate(gun.local_paxial_dir, {x=gun.player_rotation.x*math.pi/180,y=0,z=0})
local rot = vector.dir_to_rotation(dir)*180/math.pi
player:set_bone_position(self.bone_aliases.gun, {x=pos.x, y=pos.y, z=pos.z}, {x=-rot.x,y=-rot.y,z=0})
pos.x = (pos.x/10)*vs.x
pos.y = (pos.y/10)*vs.y
pos.z = (pos.z/10)*vs.z
-- minetest.chat_send_all(dump(pos))
end
function player_model:update_arm_bones(dt)
local player = self.player
local handler = self.handler
local gun = handler.gun
local pprops = handler:get_properties()
local left_bone, right_bone = vector.multiply(self.offsets.global.arm_left, pprops.visual_size), vector.multiply(self.offsets.global.arm_right, pprops.visual_size)
local left_trgt, right_trgt = gun:get_arm_aim_pos() --this gives us our offsets relative to the gun.
--get the real position of the gun's bones relative to the player (2nd param true)
left_trgt = gun:get_pos(left_trgt, true)
right_trgt = gun:get_pos(right_trgt, true)
local left_rotation = vector.dir_to_rotation(vector.direction(left_bone, left_trgt))*180/math.pi
local right_rotation = vector.dir_to_rotation(vector.direction(right_bone, right_trgt))*180/math.pi
--all of this is pure insanity. There's no logic, or rhyme or reason. Trial and error is the only way to write this garbo.
left_rotation.x = -left_rotation.x
right_rotation.x = -right_rotation.x
player:set_bone_position(self.bone_aliases.arm_left, self.offsets.relative.arm_left, {x=90, y=0, z=0}-left_rotation)
player:set_bone_position(self.bone_aliases.arm_right, self.offsets.relative.arm_right, {x=90, y=0, z=0}-right_rotation)
end
function player_model:update_head(dt)
local player = self.player
local handler = self.handler
local gun = handler.gun
player:set_bone_position(self.bone_aliases.head, self.offsets.relative.head, {x=handler.look_rotation.x,z=0,y=0})
end
--should be renamed to "release" but, whatever.
function player_model:prepare_deletion()
assert(self.instance, "attempt to call object method on a class")
local handler = Guns4d.players[self.player:get_player_name()]
local properties = handler:get_properties()
--[[if minetest.get_modpath("player_api") then
player_api.set_model(self.player, self.old)
end]]
properties.mesh = self.old
handler:set_properties(properties)
end
--todo: add value for "still_frame" (the frame to take samples from in case 0, 0 is not still.)
---@diagnostic disable-next-line: duplicate-set-field
function player_model.construct(def)
if def.instance then
assert(def.player, "no player provided")
def.handler = Guns4d.players[def.player:get_player_name()]
--set the mesh
local properties = def.handler:get_properties()
def.old = properties.mesh
properties.mesh = def.compatible_meshes[properties.mesh]
def.gun_bone_location = vector.new()
if not properties.mesh then
local fallback = def.compatible_meshes[def.fallback_mesh]
minetest.log("error", "Player model handler error: no equivelant mesh found for '"..def.old.."'. Using fallback mesh ("..fallback..")")
properties.mesh = fallback
end
def.handler:set_properties(properties)
--no further aciton required, it e
else
--none of these should consist across classes
def.offsets = {
global = {},
relative = {},
}
--a list of meshes compatible with this handler.
for og_mesh, replacement_mesh in pairs(def.compatible_meshes) do
assert(type(og_mesh)=="string", "mesh to be replaced (index) must be a string!")
if player_model.handlers[og_mesh] then minetest.log("warning", "Guns4d: mesh '"..og_mesh.."' overridden by a handler class, this will replace the old handler. Is this a mistake?") end
player_model.handlers[replacement_mesh] = def
end
--find a valid model to read.
if rawget(def, "auto_generate") then
--blame mod security, this is dumb.
assert(rawget(def, "output_path"), "a output path contained within the mod's source files is required to automatically generate models")
end
local read_model
for i, v in pairs(def.compatible_meshes) do
if (type(i)=="string") and (i~="__overfill") then
if def.auto_generate and ((not v) or not mtul.paths.media_paths[v]) then
def.compatible_meshes[i] = def:generate_b3d_model(i)
end
read_model=def.compatible_meshes[i]
end
end
assert(read_model, "at least one compatible mesh required by default for autogeneration of offsets")
local b3d_table = mtul.b3d_reader.read_model(read_model, true)
--[[all of the compatible_meshes should be identical in terms of guns4d specific bones and offsets (arms, head).
Otherwise a new handler should be different. With new compatibilities]]
for i, v in pairs(def.bone_aliases) do
--print(def.bone_aliases[v])
local node = mtul.b3d_nodes.get_node_by_name(b3d_table, v, true)
if i~="__overfill" then
local transform, _ = mtul.b3d_nodes.get_node_global_transform(node, def.still_frame)
def.offsets.relative[i] = vector.new(node.position[1], node.position[2], node.position[3])
def.offsets.global[i] = vector.new(transform[13], transform[14], transform[15])/10 --4th column first 3 rows give us our global transform.
--print(i, mtul.b3d_nodes.get_node_rotation(b3d_table, node, true, def.still_frame))
end
end
def.offsets.global.hipfire = vector.new(mtul.b3d_nodes.get_node_global_position(b3d_table, def.bone_aliases.arm_right, true, def.still_frame))
if def.is_new_default then
player_model.set_default_handler(def)
end
end
end
Guns4d.player_model_handler = mtul.class.new_class:inherit(player_model)
Guns4d.player_model_handler:set_default_handler()