New feature to allow for easier voice acting
Moderator: Forum Moderators
Re: New feature to allow for easier voice acting
I like the approach of hiding it from Lua and sticking every locale into a separate directory. I expect it will be easier to work with than encoding the locale into each and every file name.josteph wrote: ↑February 16th, 2019, 1:28 pmSo, adding a wesnoth.get_language function that returns the current language is easy (I have a prototype of that too). Is there a reason we shouldn't make that value exposed to UMC though? If so we'd have to have some sort of wesnoth.play_localized_sound function, implemented in C++, that takes a filenamefoo.ogg
and callswesnoth.play_sound "en_US/foo.ogg"
(replacing en_US by the appropriate locale), to hide the locale from Lua.
Re: New feature to allow for easier voice acting
It is not like players language is secret. It is possible to query translatable name of (unit/UI element/something else) and compare it to what it would be for each known translation.
Re: New feature to allow for easier voice acting
That will not work for random-generated names though, because that would be a huge effort to record and create a ruleset for.
Re: New feature to allow for easier voice acting
Ravana is right, lua code can already figure out the locale by doing something like
edit: Can't add this to 1.14 after all because of API compatibility rules. I can find workarounds though to allow playtesting this in 1.14.
wesnoth.textdomain("wesnoth")("Exit")
, in the Latin locale it returns Exire
for example. I added the Lua API in 2975d616dcf0dffb7450b6d4e35b7e6b679fcd4b and backported it to 1.14 in 3c3e73ac2dbcf91be8efd87e33ca7d7523042dc0. We can still use the "separate directory per language" layout but we can implement it in Lua rather than C++.edit: Can't add this to 1.14 after all because of API compatibility rules. I can find workarounds though to allow playtesting this in 1.14.
Re: New feature to allow for easier voice acting
What would the pathway look like to store the sound files?
Creator of: The Reign of The Lords Era,The Gnats Franken Dungeon.
- Celtic_Minstrel
- Developer
- Posts: 2211
- Joined: August 3rd, 2012, 11:26 pm
- Location: Canada
- Contact:
Re: New feature to allow for easier voice acting
So, the hashing idea is basically that you want to be able to not duplicate the actual campaign logic in the voice pack add-on, right? I don't think there's any good solution to this, honestly, though I see you've already talked about some not-so-good solutions - a hash-based filename (but it'd be unreadable for the voice pack developer) or a filename somehow based off the message tag contents (but sometimes the information you'd need isn't right there in the tag). Another possibility would be using the filename and line number of the string, or some kind of Xpath-type syntax. I don't think any method would be robust against changes to the source file.
A campaign could easily be designed to allow a voice-pack to be created separately, by adding
-------
Another thing that people have been talking about in this thread is the localization of the voice files. I don't think that's something that needs a new solution. The engine already has a framework for the localization of images, after all; I don't know if it currently works for sound files, but if not, I expect it would be easy to extend it so it does. With that, you don't need any
You could also just use a translatable string in the
A campaign could easily be designed to allow a voice-pack to be created separately, by adding
voice=
keys that point to nonexistent files. I suppose that's not really enough to satisfy this requirement though.-------
Another thing that people have been talking about in this thread is the localization of the voice files. I don't think that's something that needs a new solution. The engine already has a framework for the localization of images, after all; I don't know if it currently works for sound files, but if not, I expect it would be easy to extend it so it does. With that, you don't need any
wesnoth.get_language()
or wesnoth.play_localized_sound()
kind of thing.You could also just use a translatable string in the
voice=
key, and translators would substitute it with an altered path. I don't think that's a good solution though.Re: New feature to allow for easier voice acting
Neither do I. It would clutter the po files for those locales who cannot afford to do voice acting.Celtic_Minstrel wrote: ↑February 19th, 2019, 4:37 amYou could also just use a translatable string in thevoice=
key, and translators would substitute it with an altered path. I don't think that's a good solution though.
Having a non-translatable voice key sounds doable though - it could then be married with the backend the same way that translatable images work I guess.
Re: New feature to allow for easier voice acting
The_Gnat, I imagine the paths would be SomeDirectory/language code/filename.ogg, where language code is the po code (like "en" for English) and filename is what we've been discussing. I think those paths would need to be added to [binary_path] when the campaign is played? But that can probably be arranged.
CelMin, I agree there isn't a way to make the voice files work against changes to the campaign dialog. That's already a problem with regular translations, and voiceovers are really a special kind of translation.
CelMin, I agree there isn't a way to make the voice files work against changes to the campaign dialog. That's already a problem with regular translations, and voiceovers are really a special kind of translation.
Re: New feature to allow for easier voice acting
Okay, new prototype.
This is a complete example, you can apply the patch and create data/core/sounds/en/noid-d69fca0a-message_by_a.ogg as a copy of or symlink to data/core/sounds/ambient/wardrums.ogg and wesnoth will play war drums as soon as you start
It now uses a real hash, thanks jyrkive for recommending fnv1a.
It now expects filenames such as
What remains to be done is:
1. The language code shouldn't be hardcoded. In master we can use the new get_language API. In 1.14 we could hack around it with something like
2. Handle show_if and SUF's that don't match. This is easy to do, I just didn't want to duplicate code from master.
3. Make it easy to create files with the correct names, with the hash values embedded.
edit:
4. Support male_message/female_message
5. Inject
6. Other CelMin feedback https://forums.wesnoth.org/viewtopic.ph ... 26#p638815
Code: Select all
diff --git a/data/core/sounds/en/noid-d69fca0a-message_by_a.ogg b/data/core/sounds/en/noid-d69fca0a-message_by_a.ogg
index e69de29bb2d..80eb869798e 120000
--- a/data/core/sounds/en/noid-d69fca0a-message_by_a.ogg
+++ b/data/core/sounds/en/noid-d69fca0a-message_by_a.ogg
@@ -0,0 +1 @@
+../ambient/wardrums.ogg
\ No newline at end of file
diff --git a/data/scenario-test.cfg b/data/scenario-test.cfg
index 778cf5af56a..99d750b89e1 100644
--- a/data/scenario-test.cfg
+++ b/data/scenario-test.cfg
@@ -1437,6 +1437,62 @@ My best advancement costs $next_cost gold and I’m $experience|% there."
[event]
name=start
+ [lua]
+ code = <<
+
+local function fnv1a(data)
+ -- http://isthe.com/chongo/tech/comp/fnv/#FNV-1a
+ hash = 2166136261
+ for i = 1, #data do
+ hash = hash ~ data:byte(i)
+ hash = hash * 16777619
+ end
+ hash = hash & 0xFFFFFFFF
+ return string.format('%x', hash)
+end
+-- assert (fnv1a("foo") == "a9f37ed7")
+
+ old_message = wesnoth.wml_actions.message
+ function wesnoth.wml_actions.message(cfg)
+ -- TODO We should use the original message as in the scenario file here, the untranslated one, but that's not accessible. To access it we should expose tstring::value() or tstring::base_str() to lua, I think?
+ local localized_message = tostring(cfg.message)
+
+ -- Speaker may be specified by id or by SUF.
+ local speaker = cfg.speaker or cfg.id
+ if (not speaker) or speaker:find(',') or speaker == 'narrator' or speaker == 'unit' or speaker == 'second_unit' then
+ -- Keep the list of exceptions in sync with get_speaker in data/lua/wml/message.lua
+ speaker = 'noid'
+ end
+
+ local hash = fnv1a(localized_message)
+ -- Uncomment during development for testing
+ -- hash = '00000000'
+
+ -- Target language. We ought to obtain this from somewhere.
+ -- TODO We could use get_language https://github.com/wesnoth/wesnoth/commit/2975d616dcf0dffb7450b6d4e35b7e6b679fcd4b but it'd be better to implement something like https://wiki.wesnoth.org/ImageLocalization
+ local language = 'en'
+
+ -- Human-readable part.
+ -- Truncate it because Windows limits filename length
+ -- 12 was chosen arbitrarily
+ local readable = localized_message:gsub(' ', '_'):sub(1,12)
+
+ local filename = string.format("%s/%s-%s-%s.ogg", language, speaker, hash, readable)
+ -- Can't use have_file here because it has a different interface than play_sound:
+ -- have_file("core/sounds/ambient/wardrums.ogg") v. play_sound("ambient/wardrums.ogg")
+ wesnoth.message("Hello world: playing " .. filename)
+ wesnoth.play_sound(filename) -- may log "error audio: Could not loud sound file '%s'"
+
+ -- TODO handle show_if and SUF's with no matches
+ old_message(cfg)
+ end
+ >>
+ [/lua]
+ [message]
+ race = orc
+ message = _"message by an orc"
+ [/message]
+
[gold]
side=1
amount=1000
wesnoth -t
.It now uses a real hash, thanks jyrkive for recommending fnv1a.
It now expects filenames such as
en/noid-d69fca0a-message_by_a.ogg
, that is, language code, speaker's id or the string noid
, fnv1a hash of the localized string (should the file be named after the English string instead?), and the first few ASCII letters of the message. This is a directory layout similar to the localized images.What remains to be done is:
1. The language code shouldn't be hardcoded. In master we can use the new get_language API. In 1.14 we could hack around it with something like
Spoiler:
3. Make it easy to create files with the correct names, with the hash values embedded.
edit:
4. Support male_message/female_message
5. Inject
voice=
into the message. Takes care of todo #26. Other CelMin feedback https://forums.wesnoth.org/viewtopic.ph ... 26#p638815
- Celtic_Minstrel
- Developer
- Posts: 2211
- Joined: August 3rd, 2012, 11:26 pm
- Location: Canada
- Contact:
Re: New feature to allow for easier voice acting
Some miscellaneous comments...
- You shouldn't need to include the language in this calculation - the asset localization system can automatically handle that part of the task for you. It's not currently active for sound files, but it's not tricky to extend it to them (I've already done it on my local copy).
- I think for the hash you could just use
wml.tostring{message = message}
, which should return the unlocalized message. You're also using the unlocalized message in the filename, but I think that's a very bad idea as the filenames will quickly get unwieldy, and truncating it to 12 characters probably makes it almost useless. - For the filenames, I also suggest checking for a few common SUF tags such as race or unit type if speaker is missing. Furthermore, I'll note that there's no need for it to be just a file - you could produce something like
speaker/hash-excerpt.ogg
and have all messages from a given speaker grouped together. (Note that, using the built-in localization system, that would mean each speaker also has their own folder for localizations, something likespeaker/l10n/fr/hash-excerpt.ogg
.) - Actually, if you really want a message excerpt in the filename, why not take the time to code something a little more sophisticated? For example... make sure to break it between words (partial words usually won't help much anyway). Or even try to take the entire first sentence if it's short enough.
- Also regarding the filenames, you'll probably need to encode gender into it too in some cases, as a single message could have two strings, one for each gender.
- Rather than directly playing the sound, I'd recommend injecting it into the config as the
voice
key to make use of the[message]
tag's automatic spatialization of the dialogue. That also takes care of your todo about[show_if]
. - We should add a function
wesnoth.have_asset("ambient/wardrums.ogg", "sound")
or similar. Orhave_resource
or whatever you want to call it. The type should probably be optional (it can auto-detect from the file extension and default to the empty string if it doesn't recognize the extension).
Re: New feature to allow for easier voice acting
Great! I kept the language in the prototype so The_Gnat would be able to test drive it (using more or less the final directory structure) without first having to build master.Celtic_Minstrel wrote: ↑February 23rd, 2019, 4:28 am
- You shouldn't need to include the language in this calculation - the asset localization system can automatically handle that part of the task for you. It's not currently active for sound files, but it's not tricky to extend it to them (I've already done it on my local copy).
That syntax returns the localized message:Celtic_Minstrel wrote: ↑February 23rd, 2019, 4:28 am
- I think for the hash you could just use
wml.tostring{message = message}
, which should return the unlocalized message. You're also using the unlocalized message in the filename, but I think that's a very bad idea as the filenames will quickly get unwieldy, and truncating it to 12 characters probably makes it almost useless.
Code: Select all
$ wml.tostring{ foo = wesnoth.textdomain("wesnoth")("Exit") }
'foo = Exire
'
I did say it was a prototype. This is not the sort of detail I'm worried about at this stage in the lifetime of the feature. We can make this sort of change later, once the basic approach is validated.Celtic_Minstrel wrote: ↑February 23rd, 2019, 4:28 am
- Actually, if you really want a message excerpt in the filename, why not take the time to code something a little more sophisticated? For example... make sure to break it between words (partial words usually won't help much anyway). Or even try to take the entire first sentence if it's short enough.
Good point. I'll add a todo.Celtic_Minstrel wrote: ↑February 23rd, 2019, 4:28 am
- Also regarding the filenames, you'll probably need to encode gender into it too in some cases, as a single message could have two strings, one for each gender.
Will do.Celtic_Minstrel wrote: ↑February 23rd, 2019, 4:28 am
- Rather than directly playing the sound, I'd recommend injecting it into the config as the
voice
key to make use of the[message]
tag's automatic spatialization of the dialogue. That also takes care of your todo about[show_if]
.
OKCeltic_Minstrel wrote: ↑February 23rd, 2019, 4:28 am
- We should add a function
wesnoth.have_asset("ambient/wardrums.ogg", "sound")
or similar. Orhave_resource
or whatever you want to call it. The type should probably be optional (it can auto-detect from the file extension and default to the empty string if it doesn't recognize the extension).
Re: New feature to allow for easier voice acting
About this:
Does anyone have an idea how to do this? My idea is to use wmlparser3 and jq to extract all the [message] tags from WML, then to create 0-byte files with the right names. Would that be convenient for the voiceover artists who would use this?
Re: New feature to allow for easier voice acting
Sorry for multipost but here's a third prototype.
I moved the test code to AOI. It now uses voice=, uses role/race as fallback before
The filenames is looks for are:
en/orc--d69fca0a-message_by_an_orc.ogg
en/Erlornas--e485fe1-Look_at_them_Big_slow.ogg
en/advisor-male-1f7bdbd7-My_lord_none_of_our.ogg
Updated todo list
1. get_speaker is duplicated from master. Not an issue for 1.14, as get_speaker there won't change, but for master the code should call the get_speaker in data/lua/wml/message.lua
2. Consider naming the file after the English string. (if the voiceover team prefers this)
3. Make it easy to create files with the correct names, with the hash values embedded. (see my previous post)
4. Use localized assets. (waiting on celmin)
I moved the test code to AOI. It now uses voice=, uses role/race as fallback before
noid
, supports male/female messages, and truncates on a word boundary.Code: Select all
diff --git a/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.cfg b/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.cfg
index 958c510d9c7..ca261f4f11c 100644
--- a/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.cfg
+++ b/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.cfg
@@ -12,6 +12,12 @@
turns=24
next_scenario=02_Assassins
+ [lua]
+ code = <<
+ wesnoth.dofile("campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.lua")
+ >>
+ [/lua]
+
{DEFAULT_SCHEDULE}
{SCENARIO_MUSIC knolls.ogg}
diff --git a/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.lua b/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.lua
new file mode 100644
index 00000000000..16829432fe0
--- /dev/null
+++ b/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.lua
@@ -0,0 +1,104 @@
+local _ = wesnoth.textdomain("wesnoth-aoi")
+
+on_event = wesnoth.require("on_event")
+on_event("recruit", function()
+ wesnoth.wml_actions.message { race = "orc", message = _ "message by an orc" }
+end)
+
+local function fnv1a(data)
+ -- http://isthe.com/chongo/tech/comp/fnv/#FNV-1a
+ hash = 2166136261
+ for i = 1, #data do
+ hash = hash ~ data:byte(i)
+ hash = hash * 16777619
+ end
+ hash = hash & 0xFFFFFFFF
+ return string.format('%x', hash)
+end
+-- assert (fnv1a("foo") == "a9f37ed7")
+
+-- TODO duplicated from data/lua/wml/message.lua
+local function get_speaker(cfg)
+ local speaker
+ local context = wesnoth.current.event_context
+
+ if cfg.speaker == "narrator" then
+ speaker = "narrator"
+ elseif cfg.speaker == "unit" then
+ speaker = wesnoth.get_unit(context.x1 or 0, context.y1 or 0)
+ elseif cfg.speaker == "second_unit" then
+ speaker = wesnoth.get_unit(context.x2 or 0, context.y2 or 0)
+ else
+ speaker = wesnoth.get_units(cfg)[1]
+ end
+
+ return speaker
+end
+
+old_message = wesnoth.wml_actions.message
+function wesnoth.wml_actions.message(cfg)
+ if not cfg.voice then
+ -- TODO We should use the original message as in the scenario
+ -- file here, the untranslated one, but that's not accessible.
+ -- To access it we should expose tstring::value() or
+ -- tstring::base_str() to lua, I think?
+ local localized_message = tostring(cfg.message)
+
+ -- Speaker may be specified by id or by SUF. When it's
+ -- specified by SUF, the filename just says "noid".
+ local speaker_id = cfg.speaker or cfg.id or cfg.role or cfg.race
+ if (not speaker_id) or speaker_id:find(',') or speaker_id == 'narrator' or speaker_id == 'unit' or speaker_id == 'second_unit' then
+ -- Keep the list of exceptions in sync with get_speaker
+ -- in data/lua/wml/message.lua
+ speaker_id = 'noid'
+ end
+
+ -- Hash, to ensure the filenames are unique
+ local hash = fnv1a(localized_message)
+
+ -- Target language. (Temporary until we use localized asset support in the engine.)
+ local language = 'en'
+
+ -- Human-readable part.
+ local f = localized_message:gmatch('[A-Za-z]+')
+ local readable = ""
+ for i=1,5 do
+ local append = f()
+ if append ~= nil then
+ readable = readable .. '_' .. append
+ end
+ end
+ -- Remove the leading underscore, and truncate the string because Windows limits filename length
+ local readable = readable:sub(2, 100)
+
+ -- Gender, when it's not known in advance.
+ local gender = ""
+ if (cfg.male_message and cfg.female_message) or (cfg.male_message and cfg.message) or (cfg.female_message and cfg.message) then
+ -- Special case: we evaluate the SUF to determine the unit's gender in this particular playthrough.
+ local speaker_unit = get_speaker(cfg)
+ if speaker_unit == "narrator" then
+ wesnoth.log('warning', "A narrator message uses gendered messages: " .. tostring(cfg))
+ elseif speaker_unit then
+ gender = speaker_unit.gender
+ end
+ end
+
+ -- Assemble the lot into, for example,
+ -- en/Erlornas--e485fe1-Look_at_them_Big_slow.ogg
+ -- en/advisor-male-1f7bdbd7-My_lord_none_of_our.ogg
+ local filename = string.format("%s/%s-%s-%s-%s.ogg", language, speaker_id, gender, hash, readable)
+
+ -- may log "error audio: Could not load sound file 'en/noid--d69fca0a-message_by_an_orc.ogg'"
+ if cfg.__literal then
+ -- vconfig
+ local cfg2 = cfg.__literal
+ cfg2.voice = filename
+ cfg = wesnoth.tovconfig(cfg2)
+ else
+ -- regular table
+ cfg.voice = filename
+ end
+ end
+
+ old_message(cfg)
+end
diff --git a/data/core/sounds/en/orc--d69fca0a-message_by_an_orc.ogg b/data/core/sounds/en/orc--d69fca0a-message_by_an_orc.ogg
new file mode 120000
index 00000000000..80eb869798e
--- /dev/null
+++ b/data/core/sounds/en/orc--d69fca0a-message_by_an_orc.ogg
@@ -0,0 +1 @@
+../ambient/wardrums.ogg
\ No newline at end of file
en/orc--d69fca0a-message_by_an_orc.ogg
en/Erlornas--e485fe1-Look_at_them_Big_slow.ogg
en/advisor-male-1f7bdbd7-My_lord_none_of_our.ogg
Updated todo list
1. get_speaker is duplicated from master. Not an issue for 1.14, as get_speaker there won't change, but for master the code should call the get_speaker in data/lua/wml/message.lua
2. Consider naming the file after the English string. (if the voiceover team prefers this)
3. Make it easy to create files with the correct names, with the hash values embedded. (see my previous post)
4. Use localized assets. (waiting on celmin)
- Celtic_Minstrel
- Developer
- Posts: 2211
- Joined: August 3rd, 2012, 11:26 pm
- Location: Canada
- Contact:
Re: New feature to allow for easier voice acting
Looks okay to me now (though I didn't see where you're using role/race, maybe I just missed it). For localized assets, see #3935. For
So it looks like it does work, at least on master. I thought that change was also backported to 1.14, so it may also work in 1.14.6. If it wasn't backported, it probably could be.
get_speaker
, message.lua could return a table containing it.I tested on master with Spanish, with the following result:josteph wrote: ↑February 23rd, 2019, 1:56 pm That syntax returns the localized message:
Code: Select all
$ wml.tostring{ foo = wesnoth.textdomain("wesnoth")("Exit") } 'foo = Exire '
Code: Select all
$ _ = wesnoth.textdomain "wesnoth-lib"
$ _ "Close"
Cerrar
$ wml.tostring{key=_"Close"}
'#textdomain wesnoth-lib
key=_"Close"
'
Re: New feature to allow for easier voice acting
Thanks for the review. I use it in the the assignment toCeltic_Minstrel wrote: ↑February 23rd, 2019, 8:40 pm Looks okay to me now (though I didn't see where you're using role/race, maybe I just missed it).
speaker_id
. It works, you can see orc--
and advisor-male-
in the filenames the patch looks up.Thanks for the PR.
Yeah, works for me on master but not on latest 1.14. Anyone knows what commit did it? edit: e692294532f5b647ad5739614a11c1eebbf50884 but it doesn't compile when backported. edit2: Backported in be3ebd53aa71d3e4821197f34cbd89a9245d47d6, thanks celmin. It'll be in 1.14.6 tomorrow.Celtic_Minstrel wrote: ↑February 23rd, 2019, 8:40 pm I tested on master with Spanish, with the following result:
So it looks like it does work, at least on master. I thought that change was also backported to 1.14, so it may also work in 1.14.6. If it wasn't backported, it probably could be.Code: Select all
$ _ = wesnoth.textdomain "wesnoth-lib" $ _ "Close" Cerrar $ wml.tostring{key=_"Close"} '#textdomain wesnoth-lib key=_"Close" '
edit:
New prototype, determines language dynamically, thanks celmin for pointing me to the translatable string.
Code: Select all
diff --git a/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.cfg b/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.cfg
index 958c510d9c7..ca261f4f11c 100644
--- a/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.cfg
+++ b/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.cfg
@@ -12,6 +12,12 @@
turns=24
next_scenario=02_Assassins
+ [lua]
+ code = <<
+ wesnoth.dofile("campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.lua")
+ >>
+ [/lua]
+
{DEFAULT_SCHEDULE}
{SCENARIO_MUSIC knolls.ogg}
diff --git a/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.lua b/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.lua
new file mode 100644
index 00000000000..cad152c79bb
--- /dev/null
+++ b/data/campaigns/An_Orcish_Incursion/scenarios/01_Defend_the_Forest.lua
@@ -0,0 +1,106 @@
+local _ = wesnoth.textdomain("wesnoth-aoi")
+
+on_event = wesnoth.require("on_event")
+on_event("recruit", function()
+ wesnoth.wml_actions.message { race = "orc", message = _ "message by an orc" }
+end)
+
+local function fnv1a(data)
+ -- http://isthe.com/chongo/tech/comp/fnv/#FNV-1a
+ hash = 2166136261
+ for i = 1, #data do
+ hash = hash ~ data:byte(i)
+ hash = hash * 16777619
+ end
+ hash = hash & 0xFFFFFFFF
+ return string.format('%x', hash)
+end
+-- assert (fnv1a("foo") == "a9f37ed7")
+
+-- TODO duplicated from data/lua/wml/message.lua
+local function get_speaker(cfg)
+ local speaker
+ local context = wesnoth.current.event_context
+
+ if cfg.speaker == "narrator" then
+ speaker = "narrator"
+ elseif cfg.speaker == "unit" then
+ speaker = wesnoth.get_unit(context.x1 or 0, context.y1 or 0)
+ elseif cfg.speaker == "second_unit" then
+ speaker = wesnoth.get_unit(context.x2 or 0, context.y2 or 0)
+ else
+ speaker = wesnoth.get_units(cfg)[1]
+ end
+
+ return speaker
+end
+
+old_message = wesnoth.wml_actions.message
+function wesnoth.wml_actions.message(cfg)
+ if not cfg.voice then
+ -- TODO We should use the original message as in the scenario
+ -- file here, the untranslated one, but that's not accessible.
+ -- To access it we should expose tstring::value() or
+ -- tstring::base_str() to lua, I think?
+ local localized_message = tostring(cfg.message)
+
+ -- Speaker may be specified by id or by SUF. When it's
+ -- specified by SUF, the filename just says "noid".
+ local speaker_id = cfg.speaker or cfg.id or cfg.role or cfg.race
+ if (not speaker_id) or speaker_id:find(',') or speaker_id == 'narrator' or speaker_id == 'unit' or speaker_id == 'second_unit' then
+ -- Keep the list of exceptions in sync with get_speaker
+ -- in data/lua/wml/message.lua
+ speaker_id = 'noid'
+ end
+
+ -- Hash, to ensure the filenames are unique
+ local hash = fnv1a(localized_message)
+
+ -- Target language.
+ local language = wesnoth.textdomain("wesnoth-lib")("language code for localized resources^en_US")
+ -- The value may be a comma-separated string. Use the first element only.
+ language = tostring(language):gmatch('[^,]+')()
+
+ -- Human-readable part.
+ local f = localized_message:gmatch('[A-Za-z]+')
+ local readable = ""
+ for i=1,5 do
+ local append = f()
+ if append ~= nil then
+ readable = readable .. '_' .. append
+ end
+ end
+ -- Remove the leading underscore, and truncate the string because Windows limits filename length
+ local readable = readable:sub(2, 100)
+
+ -- Gender, when it's not known in advance.
+ local gender = ""
+ if (cfg.male_message and cfg.female_message) or (cfg.male_message and cfg.message) or (cfg.female_message and cfg.message) then
+ -- Special case: we evaluate the SUF to determine the unit's gender in this particular playthrough.
+ local speaker_unit = get_speaker(cfg)
+ if speaker_unit == "narrator" then
+ wesnoth.log('warning', "A narrator message uses gendered messages: " .. tostring(cfg))
+ elseif speaker_unit then
+ gender = speaker_unit.gender
+ end
+ end
+
+ -- Assemble the lot into, for example,
+ -- en/Erlornas--e485fe1-Look_at_them_Big_slow.ogg
+ -- en/advisor-male-1f7bdbd7-My_lord_none_of_our.ogg
+ local filename = string.format("l10n/%s/%s-%s-%s-%s.ogg", language, speaker_id, gender, hash, readable)
+
+ -- may log "error audio: Could not load sound file 'en/noid--d69fca0a-message_by_an_orc.ogg'"
+ if cfg.__literal then
+ -- vconfig
+ local cfg2 = cfg.__literal
+ cfg2.voice = filename
+ cfg = wesnoth.tovconfig(cfg2)
+ else
+ -- regular table
+ cfg.voice = filename
+ end
+ end
+
+ old_message(cfg)
+end
diff --git a/data/core/sounds/l10n/en_US/orc--d69fca0a-message_by_an_orc.ogg b/data/core/sounds/l10n/en_US/orc--d69fca0a-message_by_an_orc.ogg
new file mode 120000
index 00000000000..81293bbcd54
--- /dev/null
+++ b/data/core/sounds/l10n/en_US/orc--d69fca0a-message_by_an_orc.ogg
@@ -0,0 +1 @@
+../../ambient/wardrums.ogg
\ No newline at end of file