-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathinit.lua
379 lines (348 loc) · 13 KB
/
init.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
assert(modlib.version >= 103, "character_anim requires at least version rolling-103 of modlib")
local workaround_model = modlib.mod.require"workaround"
character_anim = {}
character_anim.conf = modlib.mod.configuration()
local quaternion = modlib.quaternion
-- TODO deduplicate code: move to modlib (see ghosts mod)
local media_paths = modlib.minetest.media.paths
local static_model_names = {}
local animated_model_names = {}
for name in pairs(media_paths) do
if (name:find"character" or name:find"player") and name:match"%.b3d$" then
local fixed, data = pcall(workaround_model, name)
if fixed then
local static_name = "_character_anim_" .. name
minetest.dynamic_add_media({
filename = static_name,
filedata = data,
})
static_model_names[name] = static_name
animated_model_names[static_name] = name
else
minetest.log("warning", "character_anim: failed to workaround model " .. name)
end
end
end
local function find_node(root, name)
if root.name == name then return root end
for _, child in ipairs(root.children) do
local node = find_node(child, name)
if node then return node end
end
end
local models = setmetatable({}, {__index = function(self, filename)
if animated_model_names[filename] then
return self[animated_model_names[filename]]
end
local _, ext = modlib.file.get_extension(filename)
if not ext or ext:lower() ~= "b3d" then
-- Only B3D support currently
return
end
local path = assert(media_paths[filename], filename)
local stream = io.open(path, "rb")
local model = assert(modlib.b3d.read(stream))
assert(stream:read(1) == nil, "EOF expected")
stream:close()
self[filename] = model
return model
end})
function character_anim.is_interacting(player)
local control = player:get_player_control()
return minetest.check_player_privs(player, "interact") and (control.RMB or control.LMB)
end
local function get_look_horizontal(player)
return -math.deg(player:get_look_horizontal())
end
local players = {}
character_anim.players = players
local function get_playerdata(player)
local name = player:get_player_name()
local data = players[name]
if data then return data end
-- Initialize playerdata if it doesn't already exist
data = {
interaction_time = 0,
animation_time = 0,
look_horizontal = get_look_horizontal(player),
bone_positions = {}
}
players[name] = data
return data
end
function character_anim.set_bone_override(player, bonename, position, rotation)
local value = {
position = position,
euler_rotation = rotation
}
get_playerdata(player).bone_positions[bonename] = next(value) and value
end
local function nil_default(value, default)
if value == nil then return default end
return value
end
-- Forward declaration
local handle_player_animations
-- Raw PlayerRef methods
local set_bone_position, set_animation, set_local_animation
minetest.register_on_joinplayer(function(player)
get_playerdata(player) -- Initalizes playerdata if it isn't already initialized
if not set_bone_position then
local PlayerRef = getmetatable(player)
-- Keep our model hack completely opaque to the outside world
local set_properties = PlayerRef.set_properties
function PlayerRef:set_properties(props)
if not self:is_player() then
return set_properties(self, props)
end
local old_mesh = props.mesh
props.mesh = static_model_names[old_mesh] or old_mesh
set_properties(self, props)
props.mesh = old_mesh
end
local get_properties = PlayerRef.get_properties
function PlayerRef:get_properties()
if not self:is_player() then
return get_properties(self)
end
local props = get_properties(self)
if not props then return nil end
props.mesh = animated_model_names[props.mesh] or props.mesh
return props
end
set_bone_position = PlayerRef.set_bone_position
function PlayerRef:set_bone_position(bonename, position, rotation)
if self:is_player() then
character_anim.set_bone_override(self, bonename or "",
position or {x = 0, y = 0, z = 0},
rotation or {x = 0, y = 0, z = 0})
end
return set_bone_position(self, bonename, position, rotation)
end
set_animation = PlayerRef.set_animation
function PlayerRef:set_animation(frame_range, frame_speed, frame_blend, frame_loop)
if not self:is_player() then
return set_animation(self, frame_range, frame_speed, frame_blend, frame_loop)
end
local player_animation = get_playerdata(self)
if not player_animation then
return
end
local prev_anim = player_animation.animation
local new_anim = {
nil_default(frame_range, {x = 1, y = 1}),
nil_default(frame_speed, 15),
nil_default(frame_blend, 0),
nil_default(frame_loop, true)
}
player_animation.animation = new_anim
if not prev_anim or (prev_anim[1].x ~= new_anim[1].x or prev_anim[1].y ~= new_anim[1].y) then
-- Reset animation only if the animation changed
player_animation.animation_time = 0
handle_player_animations(0, player)
elseif prev_anim[2] ~= new_anim[2] then
-- Adapt time to new speed
player_animation.animation_time = player_animation.animation_time * prev_anim[2] / new_anim[2]
end
end
local set_animation_frame_speed = PlayerRef.set_animation_frame_speed
function PlayerRef:set_animation_frame_speed(frame_speed)
if not self:is_player() then
return set_animation_frame_speed(self, frame_speed)
end
frame_speed = nil_default(frame_speed, 15)
local player_animation = get_playerdata(self)
if not player_animation then
return
end
local prev_speed = player_animation.animation[2]
player_animation.animation[2] = frame_speed
-- Adapt time to new speed
player_animation.animation_time = player_animation.animation_time * prev_speed / frame_speed
end
local get_animation = PlayerRef.get_animation
function PlayerRef:get_animation()
if not self:is_player() then
return get_animation(self)
end
local anim = get_playerdata(self).animation
if anim then
return unpack(anim, 1, 4)
end
return get_animation(self)
end
set_local_animation = PlayerRef.set_local_animation
function PlayerRef:set_local_animation(idle, walk, dig, walk_while_dig, frame_speed)
if not self:is_player() then return set_local_animation(self) end
frame_speed = frame_speed or 30
get_playerdata(self).local_animation = {idle, walk, dig, walk_while_dig, frame_speed}
end
local get_local_animation = PlayerRef.get_local_animation
function PlayerRef:get_local_animation()
if not self:is_player() then return get_local_animation(self) end
local local_anim = get_playerdata(self).local_animation
if local_anim then
return unpack(local_anim, 1, 5)
end
return get_local_animation(self)
end
end
-- First update `character_anim` with the current animation
-- which mods like `player_api` might have already set
-- (note: these two methods are already hooked)
player:set_animation(player:get_animation())
-- Then disable animation & local animation
local no_anim = {x = 0, y = 0}
set_animation(player, no_anim, 0, 0, false)
set_local_animation(player, no_anim, no_anim, no_anim, no_anim, 1)
end)
minetest.register_on_leaveplayer(function(player) players[player:get_player_name()] = nil end)
local function clamp(value, range)
if value > range.max then
return range.max
end
if value < range.min then
return range.min
end
return value
end
local function normalize_angle(angle)
return ((angle + 180) % 360) - 180
end
local function normalize_rotation(euler_rotation)
return vector.apply(euler_rotation, normalize_angle)
end
function handle_player_animations(dtime, player)
local mesh
do
local props = player:get_properties()
if not props then
-- HACK inside on_joinplayer, the player object may be invalid
-- causing get_properties() to return nothing - just ignore this
return
end
mesh = props.mesh
end
if static_model_names[mesh] then
player:set_properties{mesh = mesh}
elseif animated_model_names[mesh] then
mesh = animated_model_names[mesh]
end
local model = models[mesh]
if not model then
return
end
local conf = character_anim.conf.models[mesh] or character_anim.conf.default
local player_animation = get_playerdata(player)
local anim = player_animation.animation
if not anim then
return
end
local range, frame_speed, _, frame_loop = unpack(anim, 1, 4)
local animation_time = player_animation.animation_time
animation_time = animation_time + dtime
player_animation.animation_time = animation_time
local range_min, range_max = range.x + 1, range.y + 1
local keyframe
if range_min == range_max then
keyframe = range_min
elseif frame_loop then
keyframe = range_min + ((animation_time * frame_speed) % (range_max - range_min))
else
keyframe = math.min(range_max, range_min + animation_time * frame_speed)
end
local bones = {}
local animated_bone_props = model:get_animated_bone_properties(keyframe, true)
local body_quaternion
for _, props in ipairs(animated_bone_props) do
local bone = props.bone_name
if bone == "Body" then
body_quaternion = props.rotation
end
local position, rotation = modlib.vector.to_minetest(props.position), props.rotation
-- Invert quaternion to match Minetest's coordinate system
rotation = {-rotation[1], -rotation[2], -rotation[3], rotation[4]}
local euler_rotation = quaternion.to_euler_rotation(rotation)
bones[bone] = {position = position, rotation = rotation, euler_rotation = euler_rotation}
end
local Body = (bones.Body or {}).euler_rotation
local Head = (bones.Head or {}).euler_rotation
local Arm_Right = (bones.Arm_Right or {}).euler_rotation
local look_vertical = math.deg(player:get_look_vertical())
if Head then Head.x = -look_vertical end
local interacting = character_anim.is_interacting(player)
if interacting and Arm_Right then
local interaction_time = player_animation.interaction_time
-- Note: -90 instead of -Arm_Right.x because it looks better
Arm_Right.x = -90 - look_vertical - math.sin(-interaction_time) * conf.arm_right.radius
Arm_Right.y = Arm_Right.y + math.cos(-interaction_time) * conf.arm_right.radius
player_animation.interaction_time = interaction_time + dtime * math.rad(conf.arm_right.speed)
else
player_animation.interaction_time = 0
end
local look_horizontal = get_look_horizontal(player)
local diff = look_horizontal - player_animation.look_horizontal
if math.abs(diff) > 180 then
diff = math.sign(-diff) * 360 + diff
end
local moving_diff = math.sign(diff) * math.abs(diff) * math.min(1, dtime / conf.body.turn_speed)
player_animation.look_horizontal = player_animation.look_horizontal + moving_diff
if math.abs(moving_diff) < 1e-6 then
player_animation.look_horizontal = look_horizontal
end
local lag_behind = diff - moving_diff
local attach_parent, _, _, attach_rotation = player:get_attach()
if attach_parent then
local parent_rotation
if attach_parent.get_rotation then
parent_rotation = attach_parent:get_rotation()
else -- 0.4.x doesn't have get_rotation(), only yaw
parent_rotation = {x = 0, y = attach_parent:get_yaw(), z = 0}
end
if attach_rotation and parent_rotation then
parent_rotation = vector.apply(parent_rotation, math.deg)
local total_rotation = normalize_rotation(vector.subtract(attach_rotation, parent_rotation))
local function rotate_relative(euler_rotation)
if not euler_rotation then return end
euler_rotation.y = euler_rotation.y + look_horizontal
local new_rotation = normalize_rotation(vector.subtract(euler_rotation, total_rotation))
modlib.table.add_all(euler_rotation, new_rotation)
end
rotate_relative(Head)
if interacting then rotate_relative(Arm_Right) end
end
elseif Body and not modlib.table.nilget(rawget(_G, "player_api"), "player_attached", player:get_player_name()) then
Body.y = Body.y + lag_behind
if Head then Head.y = Head.y + lag_behind end
if interacting and Arm_Right then Arm_Right.y = Arm_Right.y + lag_behind end
end
-- HACK this essentially only works for very character.b3d-like models;
-- it tries to find the (sole) X-rotation of the body relative to a subsequent (180°) Y-rotation.
if body_quaternion then
local body_rotation = assert(assert(find_node(model.node, "Body")).rotation)
local body_x = quaternion.to_euler_rotation(modlib.quaternion.compose(body_rotation, body_quaternion)).x
if Head then Head.x = normalize_angle(Head.x - body_x) end
if interacting and Arm_Right then Arm_Right.x = normalize_angle(Arm_Right.x - body_x) end
end
if Head then
Head.x = clamp(Head.x, conf.head.pitch)
Head.y = clamp(Head.y, conf.head.yaw)
if math.abs(Head.y) > conf.head.yaw_restriction then
Head.x = clamp(Head.x, conf.head.yaw_restricted)
end
end
if Arm_Right then Arm_Right.y = clamp(Arm_Right.y, conf.arm_right.yaw) end
-- Replace animation with serverside bone animation
for bone, values in pairs(bones) do
local overridden_values = player_animation.bone_positions[bone]
overridden_values = overridden_values or {}
set_bone_position(player, bone,
overridden_values.position or values.position,
overridden_values.euler_rotation or values.euler_rotation)
end
end
minetest.register_globalstep(function(dtime)
for _, player in pairs(minetest.get_connected_players()) do
handle_player_animations(dtime, player)
end
end)