0 |
--[[ |
1 |
Subtitles — adds subtitles to Minetest. |
2 |
|
3 |
Copyright © 2022‒2023, Silver Sandstone <@SilverSandstone@craftodon.social> |
4 |
|
5 |
Permission is hereby granted, free of charge, to any person obtaining a |
6 |
copy of this software and associated documentation files (the "Software"), |
7 |
to deal in the Software without restriction, including without limitation |
8 |
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
9 |
and/or sell copies of the Software, and to permit persons to whom the |
10 |
Software is furnished to do so, subject to the following conditions: |
11 |
|
12 |
The above copyright notice and this permission notice shall be included |
13 |
in all copies or substantial portions of the Software. |
14 |
|
15 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
16 |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
17 |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
18 |
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
19 |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
20 |
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
21 |
DEALINGS IN THE SOFTWARE. |
22 |
]] |
23 |
|
24 |
|
25 |
--- Provides the Sound class, which represents a playing sound effect. |
26 |
|
27 |
|
28 |
local ORIGIN = vector.new(0, 0, 0); |
29 |
|
30 |
|
31 |
--- Represents a playing sound effect. |
32 |
-- Sound objects should be treated as immutable, as they are shared between |
33 |
-- all players. |
34 |
-- @type Sound |
35 |
subtitles.Sound = subtitles.Object:extend(); |
36 |
|
37 |
--- Constructor. |
38 |
-- @param spec The sound's SimpleSoundSpec, as a table or string. |
39 |
-- @param parameters The sound's parameters, or nil. |
40 |
-- @param handle The sound's numeric handle, or nil if the sound is ephemeral. |
41 |
function subtitles.Sound:new(spec, parameters, handle) |
42 |
if type(spec) == 'string' then |
43 |
spec = {name = spec}; |
44 |
end; |
45 |
self.spec = spec or {}; |
46 |
self.parameters = table.copy(parameters or {}); |
47 |
self.handle = handle; |
48 |
self.ephemeral = not handle; |
49 |
|
50 |
local params = subtitles.registered_parameters[self:get_name()]; |
51 |
if params then |
52 |
for key, value in pairs(params) do |
53 |
self.parameters[key] = self.parameters[key] or value; |
54 |
end; |
55 |
end; |
56 |
end; |
57 |
|
58 |
--- Checks which players the sound is audible to. |
59 |
-- @return A sequence of player ObjectRefs. |
60 |
function subtitles.Sound:get_players() |
61 |
if self.parameters.to_player then |
62 |
return {minetest.get_player_by_name(self.parameters.to_player)}; |
63 |
elseif self.parameters.exclude_player then |
64 |
local players = {}; |
65 |
for __, player in ipairs(minetest.get_connected_players()) do |
66 |
if player:get_player_name() ~= self.parameters.exclude_player then |
67 |
table.insert(players, player); |
68 |
end; |
69 |
end; |
70 |
return players; |
71 |
else |
72 |
return minetest.get_connected_players(); |
73 |
end; |
74 |
end; |
75 |
|
76 |
--- Checks if this sound is within the player's range. |
77 |
-- @param player The player's ObjectRef. |
78 |
-- @return true if the sound is in range. |
79 |
function subtitles.Sound:is_in_range_of_player(player) |
80 |
local sound_pos = self:get_pos(); |
81 |
if not sound_pos then |
82 |
return true; |
83 |
end; |
84 |
local distance = vector.distance(player:get_pos(), sound_pos); |
85 |
return distance <= self:get_max_distance(); |
86 |
end; |
87 |
|
88 |
--- Returns the sound's maximum subtitle distance. |
89 |
-- @return The maximum distance in metres. |
90 |
function subtitles.Sound:get_max_distance() |
91 |
return tonumber(self.parameters.max_subtitle_distance) |
92 |
or tonumber(self.parameters.max_hear_distance) |
93 |
or subtitles.DEFAULT_MAX_HEAR_DISTANCE; |
94 |
end; |
95 |
|
96 |
--- Returns the sound's expected duration |
97 |
-- @return The duration in seconds. |
98 |
function subtitles.Sound:get_duration() |
99 |
local duration = tonumber(self.parameters.subtitle_duration) |
100 |
or tonumber(self.parameters.duration) |
101 |
or tonumber(self.spec.subtitle_duration) |
102 |
or tonumber(self.spec.duration); |
103 |
if duration then |
104 |
return duration; |
105 |
end; |
106 |
|
107 |
if not self.parameters.loop then |
108 |
return subtitles.DEFAULT_DURATION; |
109 |
end; |
110 |
|
111 |
return nil; |
112 |
end; |
113 |
|
114 |
--- Returns the sound's description. |
115 |
-- @return A human-readable description string. |
116 |
function subtitles.Sound:get_description() |
117 |
-- Description specified in spec or parameters: |
118 |
local desc = self.parameters.subtitle |
119 |
or self.parameters.description |
120 |
or self.spec.subtitle |
121 |
or self.spec.description; |
122 |
if desc then |
123 |
return desc; |
124 |
end; |
125 |
|
126 |
-- Registered description associated with sound name: |
127 |
desc = subtitles.registered_descriptions[self:get_name()]; |
128 |
if desc then |
129 |
return desc; |
130 |
end; |
131 |
|
132 |
-- Fallback — just show the technical name: |
133 |
local name = self:get_name(); |
134 |
subtitles.report_missing(name); |
135 |
return '<' .. name .. '>'; |
136 |
end; |
137 |
|
138 |
--- Returns the sound's technical name. |
139 |
-- @return A sound name string. |
140 |
function subtitles.Sound:get_name() |
141 |
return self.spec.name or ''; |
142 |
end; |
143 |
|
144 |
--- Calculates the sound's current position. |
145 |
-- This may change if the sound is attached to an object. |
146 |
-- @return An absolute position vector. |
147 |
function subtitles.Sound:get_pos() |
148 |
local pos; |
149 |
if self.parameters.object then |
150 |
local object_pos = self.parameters.object:get_pos(); |
151 |
if object_pos then |
152 |
self.last_object_pos = object_pos; |
153 |
else |
154 |
object_pos = self.last_object_pos; |
155 |
end; |
156 |
pos = object_pos or ORIGIN; |
157 |
end; |
158 |
if self.parameters.pos then |
159 |
pos = vector.add((pos or ORIGIN), self.parameters.pos); |
160 |
end; |
161 |
return pos; |
162 |
end; |
163 |
|
164 |
--- Returns the sound's position from the player's perspective. |
165 |
-- @param player The player's ObjectRef. |
166 |
-- @return A vector relative to the player's position and rotation. |
167 |
function subtitles.Sound:get_relative_pos_for_player(player) |
168 |
local pos = self:get_pos(); |
169 |
if not pos then |
170 |
return nil; |
171 |
end; |
172 |
|
173 |
local player_pos = player:get_pos(); |
174 |
local player_yaw = player:get_look_horizontal() or 0.0; |
175 |
pos = vector.subtract(pos, player_pos) |
176 |
pos = vector.rotate(pos, vector.new(0, -player_yaw, 0)); |
177 |
return pos; |
178 |
end; |
179 |
|
180 |
--- Returns the key to use for merging this sound with other sounds. |
181 |
-- Multiple sounds with the same merge key will be merged into a single |
182 |
-- subtitle. |
183 |
-- @return A table key, or nil to not merge. |
184 |
function subtitles.Sound:get_merge_key() |
185 |
local key = self.parameters.merge_subtitle |
186 |
or self.spec.merge_subtitle; |
187 |
if key == true then |
188 |
return self:get_name(); |
189 |
elseif key then |
190 |
return key; |
191 |
end; |
192 |
|
193 |
if self.ephemeral then |
194 |
return self:get_name(); |
195 |
end; |
196 |
|
197 |
return nil; |
198 |
end; |
199 |
|
200 |
--- Checks if this sound should be exempt from subtitles. |
201 |
-- @return true to disable the subtitle. |
202 |
function subtitles.Sound:is_exempt() |
203 |
return self.spec.no_subtitle |
204 |
or self.parameters.no_subtitle |
205 |
or self:get_name() == '' |
206 |
or self:get_description() == ''; |
207 |
end; |
208 |
|
209 |
--- Checks if this sound is dynamically positioned (attached to an object). |
210 |
-- @return true if the sound is dynamic. |
211 |
function subtitles.Sound:is_dynamic() |
212 |
return self.params.object ~= nil; |
213 |
end; |