guns4d-cd2025/classes/Player_model_handler.lua

363 lines
16 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
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_reflector_bone"
},
bone_aliases = { --names of bones used by the model handler and other parts of guns4d.
arm_right = "guns4d_arm_right", --this is a needed alias for hipfire position
arm_left = "guns4d_arm_left",
head = "guns4d_head",
gun = "guns4d_gun_bone", --another needed alias
reflector = "guns4d_reflector_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. The first on this list will be read and have it's bone offsets logged for use.
--["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"] = (leef.paths.media_paths["character.b3d"] and true) or nil, --it is compatible but it has no predefined model, one will be therefore generated using the override_bone_aliases parameters.
},
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.
-- 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)
self:update_look_offset(dt)
end
local ip_time = Guns4d.config.player_axial_interpolation_time
local ip_time2 = Guns4d.config.translation_interpolation_time
local ttransform = leef.math.mat4.identity()
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
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
local xr,yr,zr = gun:get_rotation_transform(ttransform, nil, nil, nil, nil, nil, nil, 0, nil,nil,nil):get_rot_irrlicht_bone()
pos.x = ( (player_trans.x*10) + ((gun and gun.properties.ads.horizontal_offset*10) or 0 ))/vs.x
pos.y = ( (player_trans.y*10) + (pprops.eye_height*10) )/vs.y
pos.z = ( (player_trans.z*10) )/vs.z
player:set_bone_override(self.bone_aliases.reflector,
{
position = {
vec={x=pos.x, y=pos.y, z=pos.z},
interpolation=ip_time2,
absolute = true
},
rotation = {
vec={x=xr,y=yr,z=zr},
interpolation=ip_time,
absolute = true
}
})
--hip pos is already relative to local scale
pos.x = (hip_pos.x*10*ip_inv)+( (player_trans.x*10) + ((gun and gun.properties.ads.horizontal_offset*10*ip) or 0 ))/vs.x
pos.y = (hip_pos.y*10*ip_inv)+( (player_trans.y*10) + (pprops.eye_height*10*ip) )/vs.y
pos.z = (hip_pos.z*10*ip_inv)+( (player_trans.z*10) )/vs.z
xr,yr,zr = gun:get_rotation_transform(ttransform, 0,0,0, nil,nil, nil,0, 0,0,0):get_rot_irrlicht_bone()
player:set_bone_override(self.bone_aliases.gun,
{
position = {
vec={x=pos.x, y=pos.y, z=pos.z},
interpolation=ip_time2,
absolute = true
},
rotation = {
vec={x=xr,y=yr,z=zr},
interpolation=ip_time,
absolute = true
}
})
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_look_offset(dt)
local gun = self.handler.gun
self.player:set_eye_offset(gun.total_offsets.look_trans*10)
end
function player_model:unset_look_offset()
self.player:set_eye_offset()
end
--default arm code, compatible with MTG model.
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))
local right_rotation = vector.dir_to_rotation(vector.direction(right_bone, right_trgt))
--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_override(self.bone_aliases.arm_right, {
rotation = {
vec={x=math.pi/2, y=0, z=0}-right_rotation,
absolute = true,
interpolation = .1
}
})
player:set_bone_override(self.bone_aliases.arm_left, {
rotation = {
vec={x=math.pi/2, y=0, z=0}-left_rotation,
absolute = true,
interpolation = .1
}
})
end
--updates the rotation of the head to match the gun.
function player_model:update_head(dt)
local player = self.player
local handler = self.handler
--player:set_bone_position(self.bone_aliases.head, self.offsets.relative.head, {x=handler.look_rotation.x,z=0,y=0})
player:set_bone_override(self.bone_aliases.head, {
rotation = {
vec={x=handler.look_rotation.x*math.pi/180,z=0,y=0},
absolute = true,
interpolation = .5
}
})
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]]
self:unset_look_offset()
local player = self.player
player:set_bone_override(self.bone_aliases.arm_left, {})
player:set_bone_override(self.bone_aliases.arm_right, {})
player:set_bone_override(self.bone_aliases.head, {})
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.)
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
--generates a new guns4d model bases off of the `new_bones` and `bone_overrides` parameters if one does not already exist.
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.")
--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 = leef.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~="__replace_old_table" 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
--call custom generation parameters...
b3d=self:custom_b3d_generation_parameters(b3d)
--write temp model
local writefile = io.open(new_path, "w+b")
leef.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)
leef.paths.media_paths[filename] = new_path
leef.paths.modname_by_media[filename] = "guns4d"
return filename
end
---@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
-- character.b3d (from player_api) not present, ignore generation.
elseif (def~=player_model) or (minetest.get_modpath("player_api")) then
for og_mesh, replacement_mesh in pairs(def.compatible_meshes) do
assert(type(og_mesh)=="string", "mesh index to be replaced in compatible_meshes 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" then
if (v==true) then
if def.auto_generate then
def.compatible_meshes[i] = def:generate_b3d_model(i)
elseif i~=def.fallback_mesh then
def.compatible_meshes[i] = def.compatible_meshes[def.fallback_mesh]
else
error("improperly set list of compatible_meshes in a player_model_handler inherited class. Fallback mesh "..def.fallback_mesh.." has no assigned mesh and auto_generate is off")
end
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 = leef.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
local node = leef.b3d_nodes.get_node_by_name(b3d_table, v, true)
assert(node, "player model handler: no node found by the name of \""..v.."\" check that it is the correct value, or that it has been correctly overriden to use that name.")
local transform, _ = leef.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, leef.b3d_nodes.get_node_rotation(b3d_table, node, true, def.still_frame))
end
def.offsets.global.hipfire = vector.new(leef.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 = leef.class.new_class:inherit(player_model)
Guns4d.player_model_handler:set_default_handler()