New feature to allow for easier voice acting

Discussion of all aspects of the game engine, including development of new and existing features.

Moderator: Forum Moderators

User avatar
GunChleoc
Translator
Posts: 506
Joined: September 28th, 2012, 7:35 am
Contact:

Re: New feature to allow for easier voice acting

Post by GunChleoc »

josteph wrote: February 16th, 2019, 1:28 pm
josteph wrote: February 15th, 2019, 3:00 pm TODO items:

1. Figure out the target language
So, 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 filename foo.ogg and calls wesnoth.play_sound "en_US/foo.ogg" (replacing en_US by the appropriate locale), to hide the locale from Lua.
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.
User avatar
Ravana
Forum Moderator
Posts: 2952
Joined: January 29th, 2012, 12:49 am
Location: Estonia
Contact:

Re: New feature to allow for easier voice acting

Post by Ravana »

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.
User avatar
GunChleoc
Translator
Posts: 506
Joined: September 28th, 2012, 7:35 am
Contact:

Re: New feature to allow for easier voice acting

Post by GunChleoc »

That will not work for random-generated names though, because that would be a huge effort to record and create a ruleset for.
User avatar
josteph
Inactive Developer
Posts: 741
Joined: August 19th, 2017, 6:58 pm

Re: New feature to allow for easier voice acting

Post by josteph »

Ravana is right, lua code can already figure out the locale by doing something like 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.
User avatar
The_Gnat
Posts: 2215
Joined: October 10th, 2016, 3:06 am
Contact:

Re: New feature to allow for easier voice acting

Post by The_Gnat »

josteph wrote: February 17th, 2019, 4:50 pm 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.
What would the pathway look like to store the sound files?
User avatar
Celtic_Minstrel
Developer
Posts: 2166
Joined: August 3rd, 2012, 11:26 pm
Location: Canada
Contact:

Re: New feature to allow for easier voice acting

Post by Celtic_Minstrel »

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 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.
Author of The Black Cross of Aleron campaign and Default++ era.
Former maintainer of Steelhive.
User avatar
GunChleoc
Translator
Posts: 506
Joined: September 28th, 2012, 7:35 am
Contact:

Re: New feature to allow for easier voice acting

Post by GunChleoc »

Celtic_Minstrel wrote: February 19th, 2019, 4:37 amYou 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.
Neither do I. It would clutter the po files for those locales who cannot afford to do voice acting.

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.
User avatar
josteph
Inactive Developer
Posts: 741
Joined: August 19th, 2017, 6:58 pm

Re: New feature to allow for easier voice acting

Post by josteph »

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.
User avatar
josteph
Inactive Developer
Posts: 741
Joined: August 19th, 2017, 6:58 pm

Re: New feature to allow for easier voice acting

Post by josteph »

Okay, new prototype.

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
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 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:
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 voice= into the message. Takes care of todo #2

6. Other CelMin feedback https://forums.wesnoth.org/viewtopic.ph ... 26#p638815
User avatar
Celtic_Minstrel
Developer
Posts: 2166
Joined: August 3rd, 2012, 11:26 pm
Location: Canada
Contact:

Re: New feature to allow for easier voice acting

Post by Celtic_Minstrel »

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 like speaker/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. Or have_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).
Author of The Black Cross of Aleron campaign and Default++ era.
Former maintainer of Steelhive.
User avatar
josteph
Inactive Developer
Posts: 741
Joined: August 19th, 2017, 6:58 pm

Re: New feature to allow for easier voice acting

Post by josteph »

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).
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
  • 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.
That syntax returns the localized message:

Code: Select all

$ wml.tostring{ foo = wesnoth.textdomain("wesnoth")("Exit") }
'foo = Exire
'
I don't know if I should use the localized or unlocalized message in the filename. I would be happy to use the unlocalized message if it's possible and it's what the voiceover team wants. Likewise for the truncating, if the voiceover team wants me to truncate later I will change that.
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.
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
  • 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.
Good point. I'll add a todo.
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].
Will do.
Celtic_Minstrel wrote: February 23rd, 2019, 4:28 am
  • We should add a function wesnoth.have_asset("ambient/wardrums.ogg", "sound") or similar. Or have_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).
OK
User avatar
josteph
Inactive Developer
Posts: 741
Joined: August 19th, 2017, 6:58 pm

Re: New feature to allow for easier voice acting

Post by josteph »

About this:
josteph wrote: February 22nd, 2019, 4:02 pm 3. Make it easy to create files with the correct names, with the hash values embedded.
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?
User avatar
josteph
Inactive Developer
Posts: 741
Joined: August 19th, 2017, 6:58 pm

Re: New feature to allow for easier voice acting

Post by josteph »

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 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
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)
User avatar
Celtic_Minstrel
Developer
Posts: 2166
Joined: August 3rd, 2012, 11:26 pm
Location: Canada
Contact:

Re: New feature to allow for easier voice acting

Post by Celtic_Minstrel »

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 get_speaker, message.lua could return a table containing it.
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
'
I tested on master with Spanish, with the following result:

Code: Select all

$ _ = wesnoth.textdomain "wesnoth-lib"
$ _ "Close"
Cerrar
$ wml.tostring{key=_"Close"}
'#textdomain wesnoth-lib
key=_"Close"
'
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.
Author of The Black Cross of Aleron campaign and Default++ era.
Former maintainer of Steelhive.
User avatar
josteph
Inactive Developer
Posts: 741
Joined: August 19th, 2017, 6:58 pm

Re: New feature to allow for easier voice acting

Post by josteph »

Celtic_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).
Thanks for the review. I use it in the the assignment to speaker_id. It works, you can see orc-- and advisor-male- in the filenames the patch looks up.
Celtic_Minstrel wrote: February 23rd, 2019, 8:40 pm For localized assets, see #3935.
Thanks for the PR.
Celtic_Minstrel wrote: February 23rd, 2019, 8:40 pm I tested on master with Spanish, with the following result:

Code: Select all

$ _ = wesnoth.textdomain "wesnoth-lib"
$ _ "Close"
Cerrar
$ wml.tostring{key=_"Close"}
'#textdomain wesnoth-lib
key=_"Close"
'
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.
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.

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
Post Reply