Files
OpenRA/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs
RoosterDragon ab28e6a75a Improve Lua type documentation and bindings.
The ExtractEmmyLuaAPI utility command, invoked with `--emmy-lua-api`, produces a documentation file that is used by the [OpenRA Lua Language Extension](https://marketplace.visualstudio.com/items?itemName=openra.vscode-openra-lua) to provide documentation and type information is VSCode and VSCode compatible editors when editing the Lua scripts.

We improve the documentation and types produced by this utility in a few ways:
- Require descriptions to be provided for all items.
- Fix the type definitions of the base engine types (cpos, wpos, wangle, wdist, wvec, cvec) to match with the actual bindings on the C# side. Add some extra bindings for these types to increase their utility.
- Introduce ScriptEmmyTypeOverrideAttribute to allow the C# side of the bindings to provide a more specific type. The utility command now requires this to be used to avoid accidentally exporting poor type information.
- Fix a handful of scripts where the new type information revealed warnings.

The ability to ScriptEmmyTypeOverrideAttribute allows parameters and return types to provide a more specific type compared to the previous, weak, type definition. For example LuaValue mapped to `any`, LuaTable mapped to `table`, and LuaFunction mapped to `function`. These types are all non-specific. `any` can be anything, `table` is a table without known types for its keys or values, `function` is a function with an unknown signature.

Now, we can provide specific types. , e.g. instead of `table`, ReinforcementsGlobal.ReinforceWithTransport is able to specify `{ [1]: actor, [2]: actor[] }` - a table with keys 1 and 2, whose values are an actor, and a table of actors respectively. The callback functions in MapGlobal now have signatures, e.g. instead of `function` we have `fun(a: actor):boolean`. In UtilsGlobal, we also make use of generic types. These work in a similar fashion to generics in C#. These methods operate on collections, we can introduce a generic parameter named `T` for the type of the items in those collections. Now the return type and callback parameters can also use that generic type. This means the return type or callback functions operate on the same type as whatever type is in the collection you pass in. e.g. Utils.Do accepts a collection typed as `T[]` with a callback function invoked on each item typed as `fun(item: T)`. If you pass in actors, the callback operates on an actor. If you pass in strings, the callback operates on a string, etc.

Overall, these changes should result in an improved user experience for those editing OpenRA Lua scripts in a compatible IDE.
2024-08-03 19:12:51 +03:00

183 lines
5.4 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using Eluant;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Media")]
public class MediaGlobal : ScriptGlobal
{
readonly World world;
readonly MusicPlaylist playlist;
public MediaGlobal(ScriptContext context)
: base(context)
{
world = context.World;
playlist = world.WorldActor.Trait<MusicPlaylist>();
}
[Desc("Play an announcer voice listed in notifications.yaml")]
public void PlaySpeechNotification(Player player, string notification)
{
Game.Sound.PlayNotification(world.Map.Rules, player, "Speech", notification, player?.Faction.InternalName);
}
[Desc("Play a sound listed in notifications.yaml")]
public void PlaySoundNotification(Player player, string notification)
{
Game.Sound.PlayNotification(world.Map.Rules, player, "Sounds", notification, player?.Faction.InternalName);
}
[Desc("Play a sound file")]
public void PlaySound(string file)
{
// TODO: Investigate how scripts use this function, and think about exposing the UI vs World distinction if needed
Game.Sound.Play(SoundType.World, file);
}
[Desc("Play track defined in music.yaml or map.yaml, or keep track empty for playing a random song.")]
public void PlayMusic(string track = null, [ScriptEmmyTypeOverride("fun()")] LuaFunction onPlayComplete = null)
{
if (!playlist.IsMusicAvailable)
return;
var musicInfo = !string.IsNullOrEmpty(track)
? GetMusicTrack(track)
: playlist.GetNextSong();
var onComplete = WrapOnPlayComplete(onPlayComplete);
playlist.Play(musicInfo, onComplete);
}
[Desc("Play track defined in music.yaml or map.yaml as background music." +
" If music is already playing use Media.StopMusic() to stop it" +
" and the background music will start automatically." +
" Keep the track empty to disable background music.")]
public void SetBackgroundMusic(string track = null)
{
if (!playlist.IsMusicAvailable)
return;
playlist.SetBackgroundMusic(string.IsNullOrEmpty(track) ? null : GetMusicTrack(track));
}
MusicInfo GetMusicTrack(string track)
{
var music = world.Map.Rules.Music;
if (music.ContainsKey(track))
return music[track];
Log.Write("lua", "Missing music track: " + track);
return null;
}
[Desc("Stop the current song.")]
public void StopMusic()
{
playlist.Stop();
}
[Desc("Play a video fullscreen. File name has to include the file extension.")]
public void PlayMovieFullscreen(string videoFileName, [ScriptEmmyTypeOverride("fun()")] LuaFunction onPlayComplete = null)
{
var onComplete = WrapOnPlayComplete(onPlayComplete);
Media.PlayFMVFullscreen(world, videoFileName, onComplete);
}
[Desc("Play a video in the radar window. File name has to include the file extension.")]
public void PlayMovieInRadar(string videoFileName, [ScriptEmmyTypeOverride("fun()")] LuaFunction onPlayComplete = null)
{
var onComplete = WrapOnPlayComplete(onPlayComplete);
Media.PlayFMVInRadar(videoFileName, onComplete);
}
[Desc("Display a text message to all players.")]
public void DisplayMessage(string text, string prefix = "Mission", Color? color = null)
{
if (string.IsNullOrEmpty(text))
return;
var c = color ?? Color.White;
TextNotificationsManager.AddMissionLine(prefix, text, c);
}
[Desc("Display a text message only to this player.")]
public void DisplayMessageToPlayer(Player player, string text, string prefix = "Mission", Color? color = null)
{
if (world.LocalPlayer != player)
return;
DisplayMessage(text, prefix, color);
}
[Desc("Display a system message to the player. If 'prefix' is nil the default system prefix is used.")]
public void DisplaySystemMessage(string text, string prefix = null)
{
if (string.IsNullOrEmpty(text))
return;
if (string.IsNullOrEmpty(prefix))
TextNotificationsManager.AddSystemLine(text);
else
TextNotificationsManager.AddSystemLine(prefix, text);
}
[Desc("Displays a debug message to the player, if \"Show Map Debug Messages\" is checked in the settings.")]
public void Debug(string format)
{
if (string.IsNullOrEmpty(format) || !Game.Settings.Debug.LuaDebug)
return;
TextNotificationsManager.Debug(format);
}
[Desc("Display a text message at the specified location.")]
public void FloatingText(string text, WPos position, int duration = 30, Color? color = null)
{
if (string.IsNullOrEmpty(text) || !world.Map.Contains(world.Map.CellContaining(position)))
return;
var c = color ?? Color.White;
world.AddFrameEndTask(w => w.Add(new FloatingText(position, c, text, duration)));
}
Action WrapOnPlayComplete(LuaFunction onPlayComplete)
{
if (onPlayComplete != null)
{
var f = (LuaFunction)onPlayComplete.CopyReference();
return () =>
{
try
{
using (f)
f.Call().Dispose();
}
catch (LuaException e)
{
Context.FatalError(e);
}
};
}
else
return () => { };
}
}
}