Compare commits

...

19 Commits

Author SHA1 Message Date
Gustas
426494d4eb Add MacOS Utility executable 2023-02-18 17:20:04 +02:00
Gustas
216e64a870 Polish encyclopedia descriptions 2023-02-18 16:17:58 +01:00
Gustas
e8f8f7c730 Fix replays continuing after desync 2023-02-18 15:54:34 +01:00
Gustas
93d10f658e Move force pausing to EndGame method 2023-02-18 15:54:34 +01:00
Gustas
b5783428be Return invalid path instead of crashing on HPF failing 2023-02-18 15:44:08 +01:00
penev92
8ae5561e73 Minor D2k encyclopedia UI polish
- Make the text area taller so all the current text fits without a scroll.
- Limit label width so text doesn't press against the scrollbar.
2023-02-18 15:40:35 +01:00
penev92
64606d4ede Change the way the output directory is set during packaging 2023-02-18 15:19:40 +02:00
penev92
7867eb99c6 Added video player background for D2k 2023-02-18 14:12:21 +01:00
penev92
7bc2768dc4 Added VideoPlayerWidget.LoadAndPlayAsync method
Previous commits removed the async loading of videos, which can be a problem for videos played in the radar widget mid-game because it can cause a lag spike. This loads the video on a separate thread and runs it on the main thread whenever it is loaded, not blocking the main thread meanwhile and allowing the game to continue while the video loads.
Also add back cancelling of already playing video and add a check to not try to run onComplete if it is null.
2023-02-18 14:12:19 +01:00
penev92
1f63294503 Rename some VideoPlayerWidget methods for clarity 2023-02-18 14:12:18 +01:00
penev92
d99991d317 Fix crash caused by VideoPlayerWidget.DrawOverlay
Somehow this would divide by 0 on the next line, not crashing but resulting in an integer overflow on `overlayHeight`, which would crash when trying to render.
2023-02-18 14:12:17 +01:00
penev92
d7b2727d16 Unify MediaGlobal/Scripting.Media PlayFMV* methods
- Improve consistency
- Remove dead code
- Removed AsyncAction!
- Delegate.BeginInvoke is unsupported since .NET Core and throws an "Operation is not supported on this platform." exception.
2023-02-18 14:12:12 +01:00
penev92
44f35729e1 Did a slight refactor pass on MediaGlobal
- Unified onPlayComplete callback handling to avoid code duplication.
- Fixed code style issues.
2023-02-18 14:11:01 +01:00
Paul Chote
7c9dcebbd8 Remove dependency on VCRUNTIME140.dll. 2023-02-18 14:32:33 +02:00
Matthias Mailänder
fc94d93fc8 Bump checkout action.
(cherry picked from commit b0aea7b810)
2023-02-17 12:01:30 +02:00
Matthias Mailänder
810ba2c9ae Bump setup .NET action.
(cherry picked from commit f65c3a09db)
2023-02-17 12:01:27 +02:00
Paul Chote
77190ed1ac Disable max order length check for local servers.
(cherry picked from commit 947f53a991)
2023-02-14 09:35:46 +01:00
abcdefg30
b543f738db Adjust the encyclopedia descriptions of combat tanks
(cherry picked from commit 835537fcc2)
2023-02-14 09:35:46 +01:00
abcdefg30
40a9b47b44 Adjust the encyclopedia descriptions of light armored units
(cherry picked from commit 08ba35763c)
2023-02-14 09:35:46 +01:00
27 changed files with 440 additions and 247 deletions

View File

@@ -12,10 +12,10 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
@@ -35,7 +35,7 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Check Code
run: |
@@ -53,10 +53,10 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'

View File

@@ -15,12 +15,12 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Clone Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.tag }}
- name: Install .NET 6
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
@@ -29,7 +29,7 @@ jobs:
make all
- name: Clone Wiki
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: openra/openra.wiki
token: ${{ secrets.DOCS_TOKEN }}
@@ -66,12 +66,12 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Clone Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.tag }}
- name: Install .NET 6
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
@@ -81,7 +81,7 @@ jobs:
- name: Clone docs.openra.net (Playtest)
if: startsWith(github.event.inputs.tag, 'playtest-')
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: openra/docs
token: ${{ secrets.DOCS_TOKEN }}
@@ -90,7 +90,7 @@ jobs:
- name: Clone docs.openra.net (Release)
if: startsWith(github.event.inputs.tag, 'release-')
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: openra/docs
token: ${{ secrets.DOCS_TOKEN }}

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Clone Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -37,10 +37,10 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Clone Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
@@ -66,10 +66,10 @@ jobs:
runs-on: macos-11
steps:
- name: Clone Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
@@ -101,10 +101,10 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Clone Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'

View File

@@ -1,39 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
* 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 System.Collections.Generic;
using OpenRA.Graphics;
namespace OpenRA.Effects
{
public class AsyncAction : IEffect
{
readonly Action a;
readonly IAsyncResult ar;
public AsyncAction(IAsyncResult ar, Action a)
{
this.a = a;
this.ar = ar;
}
public void Tick(World world)
{
if (ar.IsCompleted)
{
world.AddFrameEndTask(w => { w.Remove(this); a(); });
}
}
public IEnumerable<IRenderable> Render(WorldRenderer r) { yield break; }
}
}

View File

@@ -57,7 +57,9 @@ namespace OpenRA.Network
bool generateSyncReport = false;
int sentOrdersFrame = 0;
float tickScale = 1f;
bool outOfSync = false;
/// <Remarks> Should only be set in <see cref="OutOfSync"/></Remarks>
public bool IsOutOfSync { get; private set; } = false;
public struct ClientOrder
{
@@ -72,12 +74,12 @@ namespace OpenRA.Network
void OutOfSync(int frame)
{
if (outOfSync)
if (IsOutOfSync)
return;
syncReport.DumpSyncReport(frame);
World.OutOfSync();
outOfSync = true;
IsOutOfSync = true;
TextNotificationsManager.AddSystemLine($"Out of sync in frame {frame}.\nCompare syncreport.log with other players.");
}

View File

@@ -182,7 +182,7 @@ namespace OpenRA.Network
var pause = order.TargetString == "Pause";
// Prevent injected unpause orders from restarting a finished game
if (orderManager.World.PauseStateLocked && !pause)
if (orderManager.World.IsGameOver && !pause)
break;
if (orderManager.World.Paused != pause && world != null && world.LobbyInfo.NonBotClients.Count() > 1)

View File

@@ -8,7 +8,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Linguini.Bundle" Version="0.3.1" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.19" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.20" />
<PackageReference Include="Mono.NAT" Version="3.0.3" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />

View File

@@ -223,12 +223,7 @@ namespace OpenRA.Scripting
FatalErrorOccurred = true;
World.AddFrameEndTask(w =>
{
World.EndGame();
World.SetPauseState(true);
World.PauseStateLocked = true;
});
World.AddFrameEndTask(w => World.EndGame());
}
public void RegisterMapActor(string name, Actor a)

View File

@@ -115,7 +115,7 @@ namespace OpenRA.Server
frame = BitConverter.ToInt32(bytes, 4);
state = ReceiveState.Data;
if (expectLength < 0 || expectLength > MaxOrderLength)
if (expectLength < 0 || (server.Type != ServerType.Local && expectLength > MaxOrderLength))
{
Log.Write("server", $"Closing socket connection to {EndPoint} because of excessive order length: {expectLength}");
return;

View File

@@ -68,15 +68,19 @@ namespace OpenRA
public Player LocalPlayer { get; private set; }
public event Action GameOver = () => { };
/// <Remarks> Should only be set in <see cref="EndGame"/></Remarks>
public bool IsGameOver { get; private set; }
public void EndGame()
{
if (!IsGameOver)
{
SetPauseState(true);
IsGameOver = true;
foreach (var t in WorldActor.TraitsImplementing<IGameOver>())
t.GameOver(this);
gameInfo.FinalGameTick = WorldTick;
GameOver();
}
@@ -381,7 +385,6 @@ namespace OpenRA
public bool Paused { get; internal set; }
public bool PredictedPaused { get; internal set; }
public bool PauseStateLocked { get; set; }
public int WorldTick { get; private set; }
@@ -393,7 +396,7 @@ namespace OpenRA
public void SetPauseState(bool paused)
{
if (PauseStateLocked)
if (IsGameOver)
return;
IssueOrder(Order.FromTargetString("PauseGame", paused ? "Pause" : "UnPause", false));
@@ -615,8 +618,9 @@ namespace OpenRA
public void OutOfSync()
{
EndGame();
SetPauseState(true);
PauseStateLocked = true;
// In the event the replay goes out of sync, it becomes no longer usable. For polish we permanently pause the world.
ReplayTimestep = 0;
}
}

View File

@@ -1163,9 +1163,7 @@ namespace OpenRA.Mods.Common.Pathfinder
}
if (maybeAbstractCell == null)
throw new Exception(
"The abstract path should never be searched for an unreachable point. " +
$"Cell {cell} failed lookup for an abstract cell.");
return PathGraph.PathCostForInvalidPath;
}
var abstractCell = maybeAbstractCell.Value;
@@ -1176,9 +1174,8 @@ namespace OpenRA.Mods.Common.Pathfinder
{
abstractSearch.TargetPredicate = c => c == abstractCell;
if (!abstractSearch.ExpandToTarget())
throw new Exception(
"The abstract path should never be searched for an unreachable point. " +
$"Abstract cell {abstractCell} failed to route to abstract cell.");
return PathGraph.PathCostForInvalidPath;
info = graph[abstractCell];
}

View File

@@ -10,15 +10,12 @@
#endregion
using System;
using System.IO;
using Eluant;
using OpenRA.Effects;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Scripting;
using OpenRA.Video;
namespace OpenRA.Mods.Common.Scripting
{
@@ -38,13 +35,13 @@ namespace OpenRA.Mods.Common.Scripting
[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 != null ? player.Faction.InternalName : null);
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 != null ? player.Faction.InternalName : null);
Game.Sound.PlayNotification(world.Map.Rules, player, "Sounds", notification, player?.Faction.InternalName);
}
[Desc("Play a sound file")]
@@ -55,34 +52,17 @@ namespace OpenRA.Mods.Common.Scripting
}
[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, LuaFunction func = null)
public void PlayMusic(string track = null, LuaFunction onPlayComplete = null)
{
if (!playlist.IsMusicAvailable)
return;
var musicInfo = !string.IsNullOrEmpty(track) ? GetMusicTrack(track)
var musicInfo = !string.IsNullOrEmpty(track)
? GetMusicTrack(track)
: playlist.GetNextSong();
if (func != null)
{
var f = (LuaFunction)func.CopyReference();
Action onComplete = () =>
{
try
{
using (f)
f.Call().Dispose();
}
catch (LuaException e)
{
Context.FatalError(e.Message);
}
};
playlist.Play(musicInfo, onComplete);
}
else
playlist.Play(musicInfo);
var onComplete = WrapOnPlayComplete(onPlayComplete);
playlist.Play(musicInfo, onComplete);
}
[Desc("Play track defined in music.yaml or map.yaml as background music." +
@@ -113,78 +93,18 @@ namespace OpenRA.Mods.Common.Scripting
playlist.Stop();
}
[Desc("Play a VQA video fullscreen. File name has to include the file extension.")]
public void PlayMovieFullscreen(string movie, LuaFunction func = null)
[Desc("Play a video fullscreen. File name has to include the file extension.")]
public void PlayMovieFullscreen(string videoFileName, LuaFunction onPlayComplete = null)
{
Action onCompleteFullscreen;
if (func != null)
{
var f = (LuaFunction)func.CopyReference();
onCompleteFullscreen = () =>
{
try
{
using (f)
f.Call().Dispose();
}
catch (LuaException e)
{
Context.FatalError(e.Message);
}
};
}
else
onCompleteFullscreen = () => { };
Media.PlayFMVFullscreen(world, movie, onCompleteFullscreen);
var onComplete = WrapOnPlayComplete(onPlayComplete);
Media.PlayFMVFullscreen(world, videoFileName, onComplete);
}
[Desc("Play a VQA video in the radar window. File name has to include the file extension. " +
"Returns true on success, if the movie wasn't found the function returns false and the callback is executed.")]
public bool PlayMovieInRadar(string movie, LuaFunction playComplete = null)
[Desc("Play a video in the radar window. File name has to include the file extension.")]
public void PlayMovieInRadar(string videoFileName, LuaFunction onPlayComplete = null)
{
Action onCompleteRadar;
if (playComplete != null)
{
var f = (LuaFunction)playComplete.CopyReference();
onCompleteRadar = () =>
{
try
{
using (f)
f.Call().Dispose();
}
catch (LuaException e)
{
Context.FatalError(e.Message);
}
};
}
else
onCompleteRadar = () => { };
Stream s;
try
{
s = world.Map.Open(movie);
}
catch (FileNotFoundException e)
{
Log.Write("lua", $"Couldn't play movie {e.FileName}! File doesn't exist.");
onCompleteRadar();
return false;
}
var l = new AsyncLoader(Media.LoadVideo);
var ar = l.BeginInvoke(s, null, null);
Action onLoadComplete = () =>
{
Media.StopFMVInRadar();
world.AddFrameEndTask(_ => Media.PlayFMVInRadar(l.EndInvoke(ar), onCompleteRadar));
};
world.AddFrameEndTask(w => w.Add(new AsyncAction(ar, onLoadComplete)));
return true;
var onComplete = WrapOnPlayComplete(onPlayComplete);
Media.PlayFMVInRadar(videoFileName, onComplete);
}
[Desc("Display a text message to the player.")]
@@ -193,7 +113,7 @@ namespace OpenRA.Mods.Common.Scripting
if (string.IsNullOrEmpty(text))
return;
var c = color.HasValue ? color.Value : Color.White;
var c = color ?? Color.White;
TextNotificationsManager.AddMissionLine(prefix, text, c);
}
@@ -224,10 +144,33 @@ namespace OpenRA.Mods.Common.Scripting
if (string.IsNullOrEmpty(text) || !world.Map.Contains(world.Map.CellContaining(position)))
return;
var c = color.HasValue ? color.Value : Color.White;
var c = color ?? Color.White;
world.AddFrameEndTask(w => w.Add(new FloatingText(position, c, text, duration)));
}
public delegate IVideo AsyncLoader(Stream s);
Action WrapOnPlayComplete(LuaFunction onPlayComplete)
{
Action onComplete;
if (onPlayComplete != null)
{
var f = (LuaFunction)onPlayComplete.CopyReference();
onComplete = () =>
{
try
{
using (f)
f.Call().Dispose();
}
catch (LuaException e)
{
Context.FatalError(e.Message);
}
};
}
else
onComplete = () => { };
return onComplete;
}
}
}

View File

@@ -12,21 +12,20 @@
using System;
using System.IO;
using OpenRA.Mods.Common.Widgets;
using OpenRA.Video;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Scripting
{
public static class Media
{
public static void PlayFMVFullscreen(World w, string movie, Action onComplete)
public static void PlayFMVFullscreen(World w, string videoFileName, Action onComplete)
{
var playerRoot = Game.OpenWindow(w, "FMVPLAYER");
var player = playerRoot.Get<VideoPlayerWidget>("PLAYER");
try
{
player.Load(movie);
player.LoadAndPlay(videoFileName);
}
catch (FileNotFoundException)
{
@@ -60,27 +59,10 @@ namespace OpenRA.Mods.Common.Scripting
});
}
public static void PlayFMVInRadar(IVideo movie, Action onComplete)
public static void PlayFMVInRadar(string videoFileName, Action onComplete)
{
var player = Ui.Root.Get<VideoPlayerWidget>("PLAYER");
player.Open(movie);
player.PlayThen(() =>
{
onComplete();
player.CloseVideo();
});
}
public static void StopFMVInRadar()
{
var player = Ui.Root.Get<VideoPlayerWidget>("PLAYER");
player.Stop();
}
public static IVideo LoadVideo(Stream s)
{
return VideoLoader.GetVideo(s, true, Game.ModData.VideoLoaders);
player.LoadAndPlayAsync(videoFileName, onComplete);
}
}
}

View File

@@ -165,12 +165,8 @@ namespace OpenRA.Mods.Common.Traits
{
Game.RunAfterDelay(Info.GameOverDelay, () =>
{
if (!Game.IsCurrentWorld(player.World))
return;
player.World.EndGame();
player.World.SetPauseState(true);
player.World.PauseStateLocked = true;
if (Game.IsCurrentWorld(player.World))
player.World.EndGame();
});
}
}

View File

@@ -550,7 +550,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
if (video != null)
{
player = panel.Get<VideoPlayerWidget>("PLAYER");
player.Load(prefix + filename);
player.LoadAndPlay(prefix + filename);
player.DrawOverlay = false;
isVideoLoaded = true;

View File

@@ -45,18 +45,21 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var speed = PlaybackSpeed.Regular;
var originalTimestep = world.Timestep;
// In the event the replay goes out of sync, it becomes no longer usable. For polish we permanently pause the world.
Func<bool> isWidgetDisabled = () => orderManager.IsOutOfSync || orderManager.NetFrameNumber >= replayNetTicks;
var pauseButton = widget.Get<ButtonWidget>("BUTTON_PAUSE");
pauseButton.IsVisible = () => world.ReplayTimestep != 0 && orderManager.NetFrameNumber < replayNetTicks;
pauseButton.IsVisible = () => world.ReplayTimestep != 0 && !isWidgetDisabled();
pauseButton.OnClick = () => world.ReplayTimestep = 0;
var playButton = widget.Get<ButtonWidget>("BUTTON_PLAY");
playButton.IsVisible = () => world.ReplayTimestep == 0 || orderManager.NetFrameNumber >= replayNetTicks;
playButton.IsVisible = () => world.ReplayTimestep == 0 || isWidgetDisabled();
playButton.OnClick = () => world.ReplayTimestep = (int)Math.Ceiling(originalTimestep * multipliers[speed]);
playButton.IsDisabled = () => orderManager.NetFrameNumber >= replayNetTicks;
playButton.IsDisabled = isWidgetDisabled;
var slowButton = widget.Get<ButtonWidget>("BUTTON_SLOW");
slowButton.IsHighlighted = () => speed == PlaybackSpeed.Slow;
slowButton.IsDisabled = () => orderManager.NetFrameNumber >= replayNetTicks;
slowButton.IsDisabled = isWidgetDisabled;
slowButton.OnClick = () =>
{
speed = PlaybackSpeed.Slow;
@@ -66,7 +69,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var normalSpeedButton = widget.Get<ButtonWidget>("BUTTON_REGULAR");
normalSpeedButton.IsHighlighted = () => speed == PlaybackSpeed.Regular;
normalSpeedButton.IsDisabled = () => orderManager.NetFrameNumber >= replayNetTicks;
normalSpeedButton.IsDisabled = isWidgetDisabled;
normalSpeedButton.OnClick = () =>
{
speed = PlaybackSpeed.Regular;
@@ -76,7 +79,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var fastButton = widget.Get<ButtonWidget>("BUTTON_FAST");
fastButton.IsHighlighted = () => speed == PlaybackSpeed.Fast;
fastButton.IsDisabled = () => orderManager.NetFrameNumber >= replayNetTicks;
fastButton.IsDisabled = isWidgetDisabled;
fastButton.OnClick = () =>
{
speed = PlaybackSpeed.Fast;
@@ -86,7 +89,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var maximumButton = widget.Get<ButtonWidget>("BUTTON_MAXIMUM");
maximumButton.IsHighlighted = () => speed == PlaybackSpeed.Maximum;
maximumButton.IsDisabled = () => orderManager.NetFrameNumber >= replayNetTicks;
maximumButton.IsDisabled = isWidgetDisabled;
maximumButton.OnClick = () =>
{
speed = PlaybackSpeed.Maximum;

View File

@@ -370,7 +370,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
StopVideo(player);
playingVideo = pv;
player.Load(video);
player.LoadAndPlay(video);
if (player.Video == null)
{

View File

@@ -10,6 +10,8 @@
#endregion
using System;
using System.IO;
using System.Threading.Tasks;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Video;
@@ -41,19 +43,72 @@ namespace OpenRA.Mods.Common.Widgets
Action onComplete;
public void Load(string filename)
/// <summary>
/// Tries to load a video from the specified file and play it. Does nothing if the file name matches the already loaded video.
/// </summary>
/// <param name="filename">Name of the file, including the extension.</param>
public void LoadAndPlay(string filename)
{
if (filename == cachedVideoFileName)
return;
cachedVideoFileName = filename;
var stream = Game.ModData.DefaultFileSystem.Open(filename);
var video = VideoLoader.GetVideo(stream, true, Game.ModData.VideoLoaders);
Open(video);
cachedVideoFileName = filename;
Play(video);
}
public void Open(IVideo video)
/// <summary>
/// Tries to load a video from the specified file and play it. Does nothing if the file name matches the already loaded video.
/// </summary>
/// <param name="filename">Name of the file, including the extension.</param>
/// <param name="after">Action to perform after the video ends.</param>
public void LoadAndPlayAsync(string filename, Action after)
{
if (filename == cachedVideoFileName)
return;
cachedVideoFileName = filename;
if (!stopped)
CloseVideo();
Task.Run(() =>
{
try
{
var stream = Game.ModData.DefaultFileSystem.Open(filename);
var video = VideoLoader.GetVideo(stream, true, Game.ModData.VideoLoaders);
// Safeguard against race conditions with two videos being loaded at the same time - prefer to play only the last one.
if (filename != cachedVideoFileName)
{
after();
return;
}
Game.RunAfterTick(() =>
{
Play(video);
PlayThen(() =>
{
after();
CloseVideo();
});
});
}
catch (FileNotFoundException)
{
after();
}
});
}
/// <summary>
/// Plays the given <see cref="IVideo"/>.
/// </summary>
/// <param name="video">An <see cref="IVideo"/> instance.</param>
public void Play(IVideo video)
{
this.video = video;
@@ -144,6 +199,13 @@ namespace OpenRA.Mods.Common.Widgets
var videoScale = Math.Min((float)RenderBounds.Width / video.Width, RenderBounds.Height / (video.Height * AspectRatio));
var halfRowHeight = (int)(videoScale * scale / 2 + 0.5f);
// If the video is "too tightly packed" into the player and there is no room for drawing an overlay disable it.
if (halfRowHeight == 0)
{
DrawOverlay = false;
return;
}
// The overlay can be minimally stored in a 1px column which is stretched to cover the full screen
var overlayHeight = (int)(RenderBounds.Height * scale / halfRowHeight);
var overlaySheetSize = new Size(1, Exts.NextPowerOf2(overlayHeight));
@@ -224,7 +286,14 @@ namespace OpenRA.Mods.Common.Widgets
Game.Sound.StopVideo();
video.Reset();
videoSprite.Sheet.GetTexture().SetData(video.CurrentFrameData, textureSize, textureSize);
Game.RunAfterTick(onComplete);
Game.RunAfterTick(() =>
{
if (onComplete != null)
{
onComplete();
onComplete = null;
}
});
}
public void CloseVideo()

View File

@@ -245,6 +245,12 @@ dialog4:
Inherits: ^Dialog
PanelRegion: 512, 387, 6, 6, 52, 52, 6, 6
# Dummy definition, partial copy of dialog because the widget definition in `common` expects it with this name.
dialog5:
Inherits: ^Dialog
Regions:
background: 1, 1, 478, 478
lobby-bits:
Inherits: ^Glyphs
Regions:

View File

@@ -54,7 +54,7 @@ Background@ENCYCLOPEDIA_PANEL:
Children:
Background@ACTOR_BG:
Width: 150
Height: 150
Height: 170
Background: dialog3
Children:
ActorPreview@ACTOR_PREVIEW:
@@ -65,13 +65,13 @@ Background@ENCYCLOPEDIA_PANEL:
ScrollPanel@ACTOR_DESCRIPTION_PANEL:
X: 150 + 10
Width: PARENT_RIGHT - 150 - 10
Height: 150
Height: 170
TopBottomSpacing: 8
Children:
Label@ACTOR_DESCRIPTION:
X: 8
Y: 8
Width: PARENT_RIGHT - 32
Width: PARENT_RIGHT - 40
VAlign: Top
Font: Regular
Button@BACK_BUTTON:

View File

@@ -20,7 +20,7 @@ light_inf:
Armament:
Weapon: LMG
Encyclopedia:
Description: Light Infantry are lightly armored foot soldiers, equipped with 9mm RP assault rifles. Light Infantry are effective against other infantry and lightly armored vehicles.\n\nLight Infantry are resistant to missiles and large caliber guns, but very vulnerable to high explosives, fire and bullet weapons.
Description: Light Infantry are lightly armored foot soldiers, equipped with 9mm RP assault rifles. Light Infantry are effective against other infantry and lightly armored vehicles.\n\nLight Infantry are resistant to missiles and large-caliber guns, but very vulnerable to high-explosives, fire and bullet weapons.
Order: 0
Category: Units
WithInfantryBody:
@@ -54,7 +54,7 @@ engineer:
CaptureTypes: building
PlayerExperience: 10
Encyclopedia:
Description: Engineers can be used to capture enemy buildings.\n\nEngineers are resistant to anti-tank weaponry but very vulnerable to high explosives, fire and bullet weapons.
Description: Engineers can be used to capture enemy buildings.\n\nEngineers are resistant to anti-tank weaponry but very vulnerable to high-explosives, fire and bullet weapons.
Order: 30
Category: Units
-RevealOnFire:
@@ -88,7 +88,7 @@ trooper:
Weapon: Bazooka
LocalOffset: 128,0,256
Encyclopedia:
Description: Armed with missile launchers, Troopers fire wire guided armor-piercing warheads. These units are particularly effective against vehicles (especially armored ones) and buildings. However, this unit is largely useless against infantry.\n\nTroopers are resistant to anti-tank weaponry but very vulnerable to high explosives, fire and bullet weapons.
Description: Armed with missile launchers, Troopers fire wire guided armor-piercing warheads. These units are particularly effective against vehicles (especially armored ones) and buildings. However, this unit is largely useless against infantry.\n\nTroopers are resistant to anti-tank weaponry but very vulnerable to high-explosives, fire and bullet weapons.
Order: 10
Category: Units
TakeCover:
@@ -178,7 +178,7 @@ fremen:
Armament@SECONDARY:
Weapon: Fremen_L
Encyclopedia:
Description: Fremen are the native desert warriors of Dune. Fremen ground units carry 10mm Assault Rifles and Rockets. Their firepower is equally effective against infantry and vehicles.\n\nFremen units are very vulnerable to high explosive and bullet weapons.
Description: Fremen are the native desert warriors of Dune. Fremen ground units carry 10mm Assault Rifles and Rockets. Their firepower is equally effective against infantry and vehicles.\n\nFremen units are very vulnerable to high-explosive and bullet weapons.
Order: 70
Category: Units
WithInfantryBody:
@@ -307,7 +307,7 @@ saboteur:
Health:
HP: 4000
Encyclopedia:
Description: The Saboteur is a special military unit acquired by House Ordos. A single Saboteur can destroy any enemy building once he moves into it, though also destroys himself! A Saboteur can be stealthed by deploying itself.\n\nThe Saboteur is resistant to anti-tank weaponry, but very vulnerable to high explosives, fire, and bullet weapons.
Description: The Saboteur is a special military unit acquired by House Ordos. A single Saboteur can destroy any enemy building once he moves into it, though also destroys himself! A Saboteur can be stealthed by deploying itself.\n\nThe Saboteur is resistant to anti-tank weaponry, but very vulnerable to high-explosives, fire, and bullet weapons.
Order: 80
Category: Units
Mobile:

View File

@@ -339,7 +339,7 @@ deathhand:
Tooltip:
Name: Death Hand
Encyclopedia:
Description: The Death Hand Missiles' warhead carries atomic cluster munitions. It detonates above the target, inflicting great damage over a wide area.
Description: The Death Hand warhead carries atomic cluster munitions. It detonates above the target, inflicting great damage over a wide area.
Order: 250
Category: Super Weapons
Buildable:

View File

@@ -832,7 +832,7 @@ large_gun_turret:
Tooltip:
Name: Rocket Turret
Encyclopedia:
Description: The substantially improved Rocket Turret has a longer range and a higher rate of fire than the Gun Turret. The Rocket Turret's advanced targeting equipment requires power to operate.\n\nThe Rocket Turret is resistant to bullet and explosive weapons, but vulnerable to missiles and high caliber guns.
Description: The substantially improved Rocket Turret has a longer range and a higher rate of fire than the Gun Turret. The Rocket Turret's advanced targeting equipment requires power to operate.\n\nThe Rocket Turret is resistant to bullet and explosive weapons, but vulnerable to missiles and high-caliber guns.
Category: Buildings
Order: 110
RequiresBuildableArea:

View File

@@ -20,7 +20,7 @@ mcv:
Armor:
Type: light
Encyclopedia:
Description: The Mobile Construction Vehicle must be driven to a suitable deployment area. After locating an appropriate area of rock, the MCV can be transformed into a Construction Yard.\n\nMCVs are resistant to bullets and some high explosives. They are vulnerable to missiles and high caliber guns.
Description: The Mobile Construction Vehicle must be driven to a suitable deployment area. After locating an appropriate area of rock, the MCV can be transformed into a Construction Yard.\n\nMCVs are resistant to bullets and light-explosives. They are vulnerable to missiles and high-caliber guns.
Order: 180
Category: Units
Mobile:
@@ -82,7 +82,7 @@ harvester:
Armor:
Type: harvester
Encyclopedia:
Description: Harvesters are resistant to bullets, and to some degree, high explosives. These units are vulnerable to missiles and high caliber guns.\n\nA Harvester is included with a Refinery.
Description: Harvesters are resistant to bullets, and to some degree, high-explosives. These units are vulnerable to missiles and high-caliber guns.\n\nA Harvester is included with a Refinery.
Order: 130
Category: Units
Mobile:
@@ -138,7 +138,7 @@ trike:
Armor:
Type: wood
Encyclopedia:
Description: Trikes are lightly armored, three-wheeled vehicles equipped with heavy machine guns, effective against infantry and lightly armored vehicles.\n\nTrikes are vulnerable to most weapons, though high caliber guns are slightly less effective against them.
Description: Trikes are lightly armored, three-wheeled vehicles equipped with heavy machine guns, effective against infantry and lightly armored vehicles.\n\nTrikes are vulnerable to most weapons, though high-caliber guns are slightly less effective against them.
Order: 90
Category: Units
Mobile:
@@ -192,7 +192,7 @@ quad:
Weapon: Rocket
LocalOffset: 128,64,64, 128,-64,64
Encyclopedia:
Description: Stronger than the Trike in both armor and firepower, the Quad is a four-wheeled vehicle firing armor-piercing rockets. The Quad is effective against most vehicles.\n\nQuads are resistant to bullets and high explosives, to a lesser degree. However, Quads are vulnerable to missiles and high caliber guns.
Description: Stronger than the Trike in both armor and firepower, the Quad is a four-wheeled vehicle firing armor-piercing rockets. The Quad is effective against most vehicles.\n\nQuads are resistant to bullets and explosives, to a lesser degree. However, Quads are vulnerable to missiles and high-caliber guns.
Order: 110
Category: Units
AttackFrontal:
@@ -227,7 +227,7 @@ siege_tank:
Armor:
Type: light
Encyclopedia:
Description: Siege Tanks are very effective against infantry and lightly armored vehicles - but very weak against heavily armored targets. They fire over a long range.\n\nSiege Tanks are resistant to bullets, and to some degree, high explosives. These units are vulnerable to missiles and high caliber guns.
Description: Siege Tanks are very effective against infantry and lightly armored vehicles - but very weak against heavily armored targets. They fire over a long range.\n\nSiege Tanks are resistant to bullets, and to some degree, explosives. These units are vulnerable to missiles and high-caliber guns.
Order: 170
Category: Units
Mobile:
@@ -287,7 +287,7 @@ missile_tank:
Armor:
Type: wood
Encyclopedia:
Description: The Missile Tank is anti-aircraft capable and effective against most targets, except infantry units.\n\nMissile Tanks are vulnerable to most weapons, though high caliber guns are slightly less effective.
Description: The Missile Tank is anti-aircraft capable and effective against most targets, except infantry units.\n\nMissile Tanks are vulnerable to most weapons, though high-caliber guns are slightly less effective.
Order: 190
Category: Units
RevealsShroud:
@@ -341,7 +341,7 @@ sonic_tank:
Weapon: Sound
LocalOffset: 600,0,427
Encyclopedia:
Description: The Sonic Tank is most effective against infantry and lightly armored vehicles - but weaker against armored targets.\n\nThe Sonic Tank will damage all units in its firing path.\n\nThey are very resistant to bullets and high explosives, but vulnerable to missiles and high caliber guns.
Description: The Sonic Tank is most effective against infantry and lightly armored vehicles - but weaker against armored targets.\n\nThe Sonic Tank will damage all units in its firing path.\n\nThey are very resistant to bullets and small-explosives, but vulnerable to missiles and high-caliber guns.
Order: 200
Category: Units
AttackFrontal:
@@ -392,7 +392,7 @@ devastator:
LocalOffset: 640,0,32
MuzzleSequence: muzzle
Encyclopedia:
Description: The Devastator is the most powerful tank on Dune - powerfully effective against most units, but slow - and slow to fire. Nuclear powered, the Devastator fires dual plasma charges and may be ordered to self-destruct, damaging surrounding units and structures.\n\nThe Devastator is very resistant to bullet and high explosives, but vulnerable to missiles and high caliber guns.
Description: The Devastator is the most powerful tank on Dune - powerfully effective against most units, but slow - and slow to fire. Nuclear powered, the Devastator fires dual plasma charges and may be ordered to self-destruct, damaging surrounding units and structures.\n\nThe Devastator is very resistant to bullet and high-explosives, but vulnerable to missiles and high-caliber guns.
Order: 220
Category: Units
AttackFrontal:
@@ -449,7 +449,7 @@ raider:
Tooltip:
Name: Raider Trike
Encyclopedia:
Description: Raiders are similar to Trikes, but the Ordos have refined their fire power, speed and armor to create a powerful and maneuverable scout. With dual 20mm cannons, Raiders are most effective against infantry and lightly armored vehicles.\n\nRaiders are vulnerable to most types of weaponry, though high caliber guns are slightly less effective.
Description: Raiders are similar to Trikes, but the Ordos have refined their fire power, speed and armor to create a powerful and maneuverable scout. With dual 20mm cannons, Raiders are most effective against infantry and lightly armored vehicles.\n\nRaiders are vulnerable to most types of weaponry, though high-caliber guns are slightly less effective.
Order: 100
Category: Units
UpdatesPlayerStatistics:
@@ -536,7 +536,7 @@ deviator:
Armor:
Type: wood
Encyclopedia:
Description: The Deviator's missiles discharge a silicon cloud that interferes with vehicle controls - temporarily changing the allegiance of the targeted unit. Personnel are not seriously effected by the cloud.\n\nThe Deviator is vulnerable to most types of weapon, though high caliber guns are slightly less effective.
Description: The Deviator's missiles discharge a silicon cloud that interferes with vehicle controls - temporarily changing the allegiance of the targeted unit. Personnel are not seriously effected by the cloud.\n\nThe Deviator is vulnerable to most types of weapon, though high-caliber guns are slightly less effective.
Order: 210
Category: Units
RevealsShroud:
@@ -611,7 +611,7 @@ combat_tank_a:
Tooltip:
Name: Atreides Combat Tank
Encyclopedia:
Description: The Combat Tank is effective against most vehicles, less so against lightly armored vehicles.\n\nAtreides Combat Tanks are very resistant to bullet and heavy explosives, but vulnerable to missiles and high caliber guns.
Description: The Combat Tank is effective against most vehicles, less so against lightly armored vehicles.\n\nAtreides Combat Tanks are very resistant to bullet and heavy-explosives, but vulnerable to missiles and high-caliber guns.
Order: 140
Category: Units
Buildable:
@@ -626,7 +626,7 @@ combat_tank_h:
Tooltip:
Name: Harkonnen Combat Tank
Encyclopedia:
Description: The Combat Tank is effective against most vehicles, less so against lightly armored vehicles.\n\nThe Harkonnen Combat Tank is stronger but slower.
Description: The Combat Tank is effective against most vehicles, less so against lightly armored vehicles.\n\nThe Harkonnen Combat Tank is stronger than its counterparts, but also slower.
Order: 160
Category: Units
Buildable:
@@ -649,7 +649,7 @@ combat_tank_o:
Turreted:
TurnSpeed: 20
Encyclopedia:
Description: The Combat Tank is effective against most vehicles, less so against lightly armored vehicles.\n\nThe Ordos Combat Tank is faster but weaker.
Description: The Combat Tank is effective against most vehicles, less so against lightly armored vehicles.\n\nThe Ordos Combat Tank is the fastest variant of the Combat Tank, but it is also the weakest.
Order: 150
Category: Units
Armament:

View File

@@ -80,7 +80,7 @@ install_assemblies() (
done
fi
else
dotnet publish -c Release -p:TargetPlatform="${TARGETPLATFORM}" -p:CopyGenericLauncher="${COPY_GENERIC_LAUNCHER}" -p:CopyCncDll="${COPY_CNC_DLL}" -p:CopyD2kDll="${COPY_D2K_DLL}" -r "${TARGETPLATFORM}" -o "${DEST_PATH}" --self-contained true
dotnet publish -c Release -p:TargetPlatform="${TARGETPLATFORM}" -p:CopyGenericLauncher="${COPY_GENERIC_LAUNCHER}" -p:CopyCncDll="${COPY_CNC_DLL}" -p:CopyD2kDll="${COPY_D2K_DLL}" -r "${TARGETPLATFORM}" -p:PublishDir="${DEST_PATH}" --self-contained true
fi
cd "${ORIG_PWD}"
)
@@ -156,7 +156,7 @@ install_windows_launcher() (
FAQ_URL="${7}"
rm -rf "${SRC_PATH}/OpenRA.WindowsLauncher/obj" || :
dotnet publish "${SRC_PATH}/OpenRA.WindowsLauncher/OpenRA.WindowsLauncher.csproj" -c Release -r "${TARGETPLATFORM}" -p:LauncherName="${LAUNCHER_NAME}" -p:TargetPlatform="${TARGETPLATFORM}" -p:ModID="${MOD_ID}" -p:DisplayName="${MOD_NAME}" -p:FaqUrl="${FAQ_URL}" -o "${DEST_PATH}" --self-contained true
dotnet publish "${SRC_PATH}/OpenRA.WindowsLauncher/OpenRA.WindowsLauncher.csproj" -c Release -r "${TARGETPLATFORM}" -p:LauncherName="${LAUNCHER_NAME}" -p:TargetPlatform="${TARGETPLATFORM}" -p:ModID="${MOD_ID}" -p:DisplayName="${MOD_NAME}" -p:FaqUrl="${FAQ_URL}" -p:PublishDir="${DEST_PATH}" --self-contained true
# NET 6 is unable to customize the application host for windows when compiling from Linux,
# so we must patch the properties we need in the PE header.

View File

@@ -124,16 +124,24 @@ modify_plist "{DEV_VERSION}" "${TAG}" "${TEMPLATE_DIR}/Contents/Info.plist"
modify_plist "{FAQ_URL}" "http://wiki.openra.net/FAQ" "${TEMPLATE_DIR}/Contents/Info.plist"
modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.11" "${TEMPLATE_DIR}/Contents/Info.plist"
# Compile universal (x86_64 + arm64) Launcher and arch-specific apphosts
# Compile universal (x86_64 + arm64) arch-specific apphosts
clang apphost.c -o "${TEMPLATE_DIR}/Contents/MacOS/apphost-x86_64" -framework AppKit -target x86_64-apple-macos10.15
clang apphost.c -o "${TEMPLATE_DIR}/Contents/MacOS/apphost-arm64" -framework AppKit -target arm64-apple-macos10.15
clang apphost-mono.c -o "${TEMPLATE_DIR}/Contents/MacOS/apphost-mono" -framework AppKit -target x86_64-apple-macos10.11
clang checkmono.c -o "${TEMPLATE_DIR}/Contents/MacOS/checkmono" -framework AppKit -target x86_64-apple-macos10.11
# Compile universal (x86_64 + arm64) Launcher
clang launcher.m -o "${TEMPLATE_DIR}/Contents/MacOS/Launcher-x86_64" -framework AppKit -target x86_64-apple-macos10.11
clang launcher.m -o "${TEMPLATE_DIR}/Contents/MacOS/Launcher-arm64" -framework AppKit -target arm64-apple-macos10.15
lipo -create -output "${TEMPLATE_DIR}/Contents/MacOS/Launcher" "${TEMPLATE_DIR}/Contents/MacOS/Launcher-x86_64" "${TEMPLATE_DIR}/Contents/MacOS/Launcher-arm64"
rm "${TEMPLATE_DIR}/Contents/MacOS/Launcher-x86_64" "${TEMPLATE_DIR}/Contents/MacOS/Launcher-arm64"
# Compile universal (x86_64 + arm64) Utility
clang utility.m -o "${TEMPLATE_DIR}/Contents/MacOS/Utility-x86_64" -framework AppKit -target x86_64-apple-macos10.11
clang utility.m -o "${TEMPLATE_DIR}/Contents/MacOS/Utility-arm64" -framework AppKit -target arm64-apple-macos10.15
lipo -create -output "${TEMPLATE_DIR}/Contents/MacOS/Utility" "${TEMPLATE_DIR}/Contents/MacOS/Utility-x86_64" "${TEMPLATE_DIR}/Contents/MacOS/Utility-arm64"
rm "${TEMPLATE_DIR}/Contents/MacOS/Utility-x86_64" "${TEMPLATE_DIR}/Contents/MacOS/Utility-arm64"
build_app "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Red Alert.app" "ra" "Red Alert" "699222659766026240"
build_app "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Tiberian Dawn.app" "cnc" "Tiberian Dawn" "699223250181292033"
build_app "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Dune 2000.app" "d2k" "Dune 2000" "712711732770111550"

227
packaging/macos/utility.m Normal file
View File

@@ -0,0 +1,227 @@
/*
* 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. For more information,
* see COPYING.
*/
#import <Cocoa/Cocoa.h>
#include <dlfcn.h>
#include <libgen.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/resource.h>
#include <mach/machine.h>
#define SYSTEM_MONO_PATH @"/Library/Frameworks/Mono.framework/Versions/Current/"
#define SYSTEM_MONO_MIN_VERSION @"6.4"
#define DOTNET_MIN_MACOS_VERSION 10.15
typedef void* hostfxr_handle;
struct hostfxr_initialize_parameters
{
size_t size;
char *host_path;
char *dotnet_root;
};
typedef int32_t(*hostfxr_initialize_for_dotnet_command_line_fn)(
int argc,
char **argv,
struct hostfxr_initialize_parameters *parameters,
hostfxr_handle *host_context_handle);
typedef int32_t(*hostfxr_run_app_fn)(const hostfxr_handle host_context_handle);
typedef int32_t(*hostfxr_close_fn)(const hostfxr_handle host_context_handle);
typedef int (*mono_main)(int argc, char **argv);
int launch_mono(int argc, char **argv, char *modId)
{
NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"];
NSString *hostPath = [SYSTEM_MONO_PATH stringByAppendingPathComponent: @"lib/libmonosgen-2.0.dylib"];
NSString *dllPath = [exePath stringByAppendingPathComponent: @"mono/OpenRA.Utility.dll"];
// TODO: This snippet increasing the open file limit was copied from
// the monodevelop launcher stub. It may not be needed for OpenRA.
struct rlimit limit;
if (getrlimit(RLIMIT_NOFILE, &limit) == 0 && limit.rlim_cur < 1024)
{
limit.rlim_cur = limit.rlim_max < 1024 ? limit.rlim_max : 1024;
setrlimit(RLIMIT_NOFILE, &limit);
}
void *libmono = dlopen([hostPath UTF8String], RTLD_LAZY);
if (!libmono)
{
NSLog(@"Failed to load libmonosgen-2.0.dylib: %s\n", dlerror());
return 1;
}
mono_main _mono_main = (mono_main)dlsym(libmono, "mono_main");
if (!_mono_main)
{
NSLog(@"Could not load mono_main(): %s\n", dlerror());
return 1;
}
// Insert hostpath, dll and modId as arguments. Overwrite the first argument which was used to launch this application.
char **newv = malloc((argc + 2) * sizeof(char*));
if (!newv)
{
NSLog(@"Failed to allocate memory for args array.\n");
return 1;
}
newv[0] = (char*)[hostPath UTF8String];
newv[1] = (char*)[dllPath UTF8String];
newv[2] = modId;
for (int i = 1; i < argc; i++)
newv[i + 2] = argv[i];
int ret = _mono_main(argc + 2, newv);
free(newv);
return ret;
}
int launch_dotnet(int argc, char **argv, char *modId, bool isArmArchitecture)
{
NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"];
NSString *dllPath;
NSString *hostPath;
if (isArmArchitecture)
{
hostPath = [exePath stringByAppendingPathComponent: @"arm64/libhostfxr.dylib"];;
dllPath = [exePath stringByAppendingPathComponent: @"arm64/OpenRA.Utility.dll"];
}
else
{
hostPath = [exePath stringByAppendingPathComponent: @"x86_64/libhostfxr.dylib"];;
dllPath = [exePath stringByAppendingPathComponent: @"x86_64/OpenRA.Utility.dll"];
}
void *lib = dlopen([hostPath UTF8String], RTLD_LAZY);
if (!lib)
{
NSLog(@"Failed to load %@: %s\n", hostPath, dlerror());
return 1;
}
hostfxr_initialize_for_dotnet_command_line_fn hostfxr_initialize_for_dotnet_command_line = (hostfxr_initialize_for_dotnet_command_line_fn)dlsym(lib, "hostfxr_initialize_for_dotnet_command_line");
if (!hostfxr_initialize_for_dotnet_command_line)
{
NSLog(@"Could not load hostfxr_initialize_for_dotnet_command_line(): %s\n", dlerror());
return 1;
}
hostfxr_run_app_fn hostfxr_run_app = (hostfxr_run_app_fn)dlsym(lib, "hostfxr_run_app");
if (!hostfxr_run_app)
{
NSLog(@"Could not load hostfxr_run_app(): %s\n", dlerror());
return 1;
}
hostfxr_close_fn hostfxr_close = (hostfxr_close_fn)dlsym(lib, "hostfxr_close");
if (!hostfxr_close)
{
NSLog(@"Could not load hostfxr_close(): %s\n", dlerror());
return 1;
}
struct hostfxr_initialize_parameters params;
params.size = sizeof(params);
params.host_path = (char*)[[exePath stringByAppendingPathComponent: @"Utility"] UTF8String];
params.dotnet_root = dirname((char*)[hostPath UTF8String]);
// Insert dll and modId as arguments. Overwrite the first argument which was used to launch this application.
char **newv = malloc((argc + 1) * sizeof(char*));
if (!newv)
{
NSLog(@"Failed to allocate memory for args array.\n");
return 1;
}
newv[0] = (char*)[dllPath UTF8String];
newv[1] = modId;
for (int i = 1; i < argc; i++)
newv[i + 1] = argv[i];
hostfxr_handle host_context_handle;
hostfxr_initialize_for_dotnet_command_line(
argc + 1,
newv,
&params,
&host_context_handle);
hostfxr_run_app(host_context_handle);
int ret = hostfxr_close(host_context_handle);
free(newv);
return ret;
}
int main(int argc, char **argv)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
size_t size;
cpu_type_t type;
size = sizeof(type);
bool isArmArchitecture = sysctlbyname("hw.cputype", &type, &size, NULL, 0) == 0 && (type & 0xFF) == CPU_TYPE_ARM;
BOOL useMono = NO;
// Before 10.15 macOS didn't support arm.
// Mono is compiled for intel only.
if (!isArmArchitecture)
{
if (@available(macOS 10.15, *))
useMono = [[[NSProcessInfo processInfo] environment]objectForKey:@"OPENRA_PREFER_MONO"] != nil;
else
useMono = YES;
}
if (useMono)
{
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/checkmono"]];
[task launch];
[task waitUntilExit];
if ([task terminationStatus] != 0)
{
NSLog(@"Utility requires Mono %@ or later. Please install Mono and try again.\n", SYSTEM_MONO_MIN_VERSION);
return 1;
}
}
NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
char *modId;
if (plist)
{
NSString *modIdValue = [plist objectForKey:@"ModId"];
if (modIdValue && [modIdValue length] > 0)
modId = (char*)[modIdValue UTF8String];
}
if (!modId)
{
NSLog(@"Could not detect ModId\n");
return 1;
}
setenv("ENGINE_DIR", (char*)[[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/Resources/"] UTF8String], 1);
int ret;
if (useMono)
ret = launch_mono(argc, argv, modId);
else
ret = launch_dotnet(argc, argv, modId, isArmArchitecture);
[pool release];
return ret;
}