Compare commits
47 Commits
prep-2307
...
playtest-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daf10c34a6 | ||
|
|
d96ec21b95 | ||
|
|
aea1182bb5 | ||
|
|
b413b97a52 | ||
|
|
7daa27f123 | ||
|
|
f3f98d8750 | ||
|
|
69eeb2dc84 | ||
|
|
7cee29ff70 | ||
|
|
5e80fee913 | ||
|
|
c1474204e2 | ||
|
|
223f9408fd | ||
|
|
674ca8555b | ||
|
|
275325b0cf | ||
|
|
06e399d3ce | ||
|
|
d6d77eab7c | ||
|
|
2392566c1d | ||
|
|
028467f150 | ||
|
|
eee4b04248 | ||
|
|
f551b542f3 | ||
|
|
5d5419702b | ||
|
|
47e89b7d76 | ||
|
|
9bb0409f19 | ||
|
|
80ac494948 | ||
|
|
eb82f2b975 | ||
|
|
a5c8db82da | ||
|
|
2a26ff6818 | ||
|
|
5a11d54956 | ||
|
|
26c5a8b76c | ||
|
|
efcfab78dc | ||
|
|
ac351f6e65 | ||
|
|
42bdc9e53e | ||
|
|
3097efc5b6 | ||
|
|
853422409f | ||
|
|
f097250394 | ||
|
|
96e0f96c12 | ||
|
|
0a4c4162be | ||
|
|
10ff24f24e | ||
|
|
aeb96d98f3 | ||
|
|
cf4bfdcdfb | ||
|
|
d55af8d75d | ||
|
|
dd9e75d017 | ||
|
|
1b6220962e | ||
|
|
f70f2acb39 | ||
|
|
32e2507bff | ||
|
|
22a42b7dc9 | ||
|
|
c01e4043e8 | ||
|
|
c8665c98a6 |
@@ -3,7 +3,7 @@ name: Continuous Integration
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [ bleed ]
|
||||
branches: [ bleed, 'prep-*' ]
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
2
.github/workflows/documentation.yml
vendored
2
.github/workflows/documentation.yml
vendored
@@ -115,7 +115,7 @@ jobs:
|
||||
cd docs
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
git add *.md
|
||||
git add api/*.md
|
||||
git commit -m "Update auto-generated documentation for ${GIT_TAG}"
|
||||
|
||||
- name: Push docs.openra.net (Release)
|
||||
|
||||
6
.github/workflows/packaging.yml
vendored
6
.github/workflows/packaging.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
file: build/linux/*
|
||||
|
||||
macos:
|
||||
name: macOS Disk Images
|
||||
name: macOS Disk Image
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Prepare Environment
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
||||
|
||||
- name: Package Disk Images
|
||||
- name: Package Disk Image
|
||||
env:
|
||||
MACOS_DEVELOPER_IDENTITY: ${{ secrets.MACOS_DEVELOPER_IDENTITY }}
|
||||
MACOS_DEVELOPER_CERTIFICATE_BASE64: ${{ secrets.MACOS_DEVELOPER_CERTIFICATE_BASE64 }}
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
mkdir -p build/macos
|
||||
./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos"
|
||||
|
||||
- name: Upload Packages
|
||||
- name: Upload Package
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
9
Makefile
9
Makefile
@@ -58,6 +58,7 @@ RM_RF = $(RM) -rf
|
||||
|
||||
RUNTIME ?= net6
|
||||
CONFIGURATION ?= Release
|
||||
DOTNET_RID = $(shell ${DOTNET} --info | grep RID: | cut -w -f3)
|
||||
|
||||
# Only for use in target version:
|
||||
VERSION := $(shell git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || (c=$$(git rev-parse --short HEAD 2>/dev/null) && echo git-$$c))
|
||||
@@ -67,15 +68,23 @@ ifndef TARGETPLATFORM
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_M := $(shell uname -m)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
ifeq ($(RUNTIME)-$(DOTNET_RID),net6-osx-arm64)
|
||||
TARGETPLATFORM = osx-arm64
|
||||
else
|
||||
TARGETPLATFORM = osx-x64
|
||||
endif
|
||||
else
|
||||
ifeq ($(UNAME_M),x86_64)
|
||||
TARGETPLATFORM = linux-x64
|
||||
else
|
||||
ifeq ($(UNAME_M),aarch64)
|
||||
TARGETPLATFORM = linux-arm64
|
||||
else
|
||||
TARGETPLATFORM = unix-generic
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
##################### DEVELOPMENT BUILDS AND TESTS #####################
|
||||
#
|
||||
|
||||
@@ -318,7 +318,7 @@ namespace OpenRA
|
||||
if (!string.IsNullOrEmpty(supportDirArg))
|
||||
Platform.OverrideSupportDir(supportDirArg);
|
||||
|
||||
Console.WriteLine($"Platform is {Platform.CurrentPlatform}");
|
||||
Console.WriteLine($"Platform is {Platform.CurrentPlatform} ({Platform.CurrentArchitecture})");
|
||||
|
||||
// Load the engine version as early as possible so it can be written to exception logs
|
||||
try
|
||||
|
||||
@@ -243,6 +243,8 @@ namespace OpenRA
|
||||
public CellRegion AllCells { get; private set; }
|
||||
public List<CPos> AllEdgeCells { get; private set; }
|
||||
|
||||
public event Action<CPos> CellProjectionChanged;
|
||||
|
||||
// Internal data
|
||||
readonly ModData modData;
|
||||
CellLayer<short> cachedTerrainIndexes;
|
||||
@@ -336,11 +338,12 @@ namespace OpenRA
|
||||
Height = new CellLayer<byte>(Grid.Type, size);
|
||||
Ramp = new CellLayer<byte>(Grid.Type, size);
|
||||
Tiles.Clear(terrainInfo.DefaultTerrainTile);
|
||||
|
||||
if (Grid.MaximumTerrainHeight > 0)
|
||||
{
|
||||
Height.CellEntryChanged += UpdateProjection;
|
||||
Tiles.CellEntryChanged += UpdateProjection;
|
||||
Tiles.CellEntryChanged += UpdateRamp;
|
||||
Tiles.CellEntryChanged += UpdateProjection;
|
||||
Height.CellEntryChanged += UpdateProjection;
|
||||
}
|
||||
|
||||
PostInit();
|
||||
@@ -530,6 +533,7 @@ namespace OpenRA
|
||||
var inverse = inverseCellProjection[uv];
|
||||
inverse.Clear();
|
||||
inverse.Add(uv);
|
||||
CellProjectionChanged?.Invoke(cell);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -567,6 +571,8 @@ namespace OpenRA
|
||||
projectedHeight[temp] = height;
|
||||
}
|
||||
}
|
||||
|
||||
CellProjectionChanged?.Invoke(cell);
|
||||
}
|
||||
|
||||
byte ProjectedCellHeightInner(PPos puv)
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace OpenRA
|
||||
|
||||
public MiniYamlNode Clone()
|
||||
{
|
||||
return new MiniYamlNode(Key, Value.Clone());
|
||||
return new MiniYamlNode(Key, Value.Clone(), Comment, Location);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,12 +57,12 @@ namespace OpenRA.Network
|
||||
|
||||
void IConnection.Send(int frame, IEnumerable<Order> o)
|
||||
{
|
||||
orders.Enqueue((frame, new OrderPacket(o.ToArray())));
|
||||
orders.Enqueue((frame, new OrderPacket(o)));
|
||||
}
|
||||
|
||||
void IConnection.SendImmediate(IEnumerable<Order> o)
|
||||
{
|
||||
immediateOrders.Enqueue(new OrderPacket(o.ToArray()));
|
||||
immediateOrders.Enqueue(new OrderPacket(o));
|
||||
}
|
||||
|
||||
void IConnection.SendSync(int frame, int syncHash, ulong defeatState)
|
||||
@@ -230,14 +230,14 @@ namespace OpenRA.Network
|
||||
|
||||
void IConnection.Send(int frame, IEnumerable<Order> orders)
|
||||
{
|
||||
var o = new OrderPacket(orders.ToArray());
|
||||
var o = new OrderPacket(orders);
|
||||
sentOrders.Enqueue((frame, o));
|
||||
Send(o.Serialize(frame));
|
||||
}
|
||||
|
||||
void IConnection.SendImmediate(IEnumerable<Order> orders)
|
||||
{
|
||||
var o = new OrderPacket(orders.ToArray());
|
||||
var o = new OrderPacket(orders);
|
||||
sentImmediateOrders.Enqueue(o);
|
||||
Send(o.Serialize(0));
|
||||
}
|
||||
|
||||
@@ -17,32 +17,32 @@ namespace OpenRA.Network
|
||||
{
|
||||
public class OrderPacket
|
||||
{
|
||||
readonly Order[] orders;
|
||||
readonly MemoryStream data;
|
||||
public OrderPacket(Order[] orders)
|
||||
public OrderPacket(IEnumerable<Order> orders)
|
||||
{
|
||||
this.orders = orders;
|
||||
data = null;
|
||||
// Orders may refer to actors that no longer exist by the time
|
||||
// that the order is resolved. In order to ensure consistent
|
||||
// behaviour between local and remote clients, it is simplest
|
||||
// to always serialize / deserialize orders, instead of storing
|
||||
// the Order objects directly on the local client.
|
||||
data = new MemoryStream();
|
||||
foreach (var o in orders)
|
||||
data.WriteArray(o.Serialize());
|
||||
}
|
||||
|
||||
public OrderPacket(MemoryStream data)
|
||||
{
|
||||
orders = null;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public IEnumerable<Order> GetOrders(World world)
|
||||
{
|
||||
return orders ?? ParseData(world);
|
||||
}
|
||||
|
||||
IEnumerable<Order> ParseData(World world)
|
||||
{
|
||||
if (data == null)
|
||||
if (data.Length == 0)
|
||||
yield break;
|
||||
|
||||
// Order deserialization depends on the current world state,
|
||||
// so must be deferred until we are ready to consume them.
|
||||
data.Position = 0;
|
||||
var reader = new BinaryReader(data);
|
||||
while (data.Position < data.Length)
|
||||
{
|
||||
@@ -54,28 +54,25 @@ namespace OpenRA.Network
|
||||
|
||||
public byte[] Serialize(int frame)
|
||||
{
|
||||
if (data != null)
|
||||
return data.ToArray();
|
||||
|
||||
var ms = new MemoryStream();
|
||||
var ms = new MemoryStream((int)data.Length + 4);
|
||||
ms.WriteArray(BitConverter.GetBytes(frame));
|
||||
foreach (var o in orders)
|
||||
ms.WriteArray(o.Serialize());
|
||||
return ms.ToArray();
|
||||
|
||||
data.Position = 0;
|
||||
data.CopyTo(ms);
|
||||
|
||||
return ms.GetBuffer();
|
||||
}
|
||||
|
||||
public static OrderPacket Combine(IEnumerable<OrderPacket> packets)
|
||||
{
|
||||
var orders = new List<Order>();
|
||||
var ms = new MemoryStream();
|
||||
foreach (var packet in packets)
|
||||
{
|
||||
if (packet.orders == null)
|
||||
throw new InvalidOperationException("OrderPacket.Combine can only be used with locally generated OrderPackets.");
|
||||
|
||||
orders.AddRange(packet.orders);
|
||||
packet.data.Position = 0;
|
||||
packet.data.CopyTo(ms);
|
||||
}
|
||||
|
||||
return new OrderPacket(orders.ToArray());
|
||||
return new OrderPacket(ms);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Linguini.Bundle" Version="0.3.1" />
|
||||
<PackageReference Include="OpenRA-Eluant" Version="1.0.18" />
|
||||
<PackageReference Include="OpenRA-Eluant" Version="1.0.19" />
|
||||
<PackageReference Include="Mono.NAT" Version="3.0.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
|
||||
@@ -13,6 +13,7 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
@@ -23,6 +24,7 @@ namespace OpenRA
|
||||
public static class Platform
|
||||
{
|
||||
public static PlatformType CurrentPlatform => LazyCurrentPlatform.Value;
|
||||
public static Architecture CurrentArchitecture => RuntimeInformation.ProcessArchitecture;
|
||||
public static readonly Guid SessionGUID = Guid.NewGuid();
|
||||
|
||||
static readonly Lazy<PlatformType> LazyCurrentPlatform = Exts.Lazy(GetCurrentPlatform);
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace OpenRA
|
||||
if (Game.ModData != null)
|
||||
{
|
||||
var mod = Game.ModData.Manifest.Metadata;
|
||||
Log.Write("exception", $"{mod.Title} mod version ${mod.Version}");
|
||||
Log.Write("exception", $"{mod.Title} mod version {mod.Version}");
|
||||
}
|
||||
|
||||
if (Game.OrderManager != null && Game.OrderManager.World != null && Game.OrderManager.World.Map != null)
|
||||
@@ -42,7 +42,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
Log.Write("exception", $"Date: {DateTime.UtcNow:u}");
|
||||
Log.Write("exception", $"Operating System: {Platform.CurrentPlatform} ({Environment.OSVersion})");
|
||||
Log.Write("exception", $"Operating System: {Platform.CurrentPlatform} ({Platform.CurrentArchitecture}, {Environment.OSVersion})");
|
||||
Log.Write("exception", $"Runtime Version: {Platform.RuntimeVersion}", Platform.RuntimeVersion);
|
||||
Log.Write("exception", $"Installed Language: {CultureInfo.InstalledUICulture.TwoLetterISOLanguageName} (Installed) {CultureInfo.CurrentCulture.TwoLetterISOLanguageName} (Current) {CultureInfo.CurrentUICulture.TwoLetterISOLanguageName} (Current UI)");
|
||||
|
||||
@@ -56,9 +56,9 @@ namespace OpenRA
|
||||
return BuildExceptionReport(ex, new StringBuilder(), 0);
|
||||
}
|
||||
|
||||
static StringBuilder AppendIndentedFormatLine(this StringBuilder sb, int indent, string format, params object[] args)
|
||||
static StringBuilder AppendIndentedLine(this StringBuilder sb, int indent, string message)
|
||||
{
|
||||
return sb.Append(new string(' ', indent * 2)).AppendFormat(format, args).AppendLine();
|
||||
return sb.Append(new string(' ', indent * 2)).Append(message).AppendLine();
|
||||
}
|
||||
|
||||
static StringBuilder BuildExceptionReport(Exception ex, StringBuilder sb, int indent)
|
||||
@@ -66,11 +66,11 @@ namespace OpenRA
|
||||
if (ex == null)
|
||||
return sb;
|
||||
|
||||
sb.AppendIndentedFormatLine(indent, $"Exception of type `{ex.GetType().FullName}`: {ex.Message}");
|
||||
sb.AppendIndentedLine(indent, $"Exception of type `{ex.GetType().FullName}`: {ex.Message}");
|
||||
|
||||
if (ex is TypeLoadException tle)
|
||||
{
|
||||
sb.AppendIndentedFormatLine(indent, $"TypeName=`{tle.TypeName}`");
|
||||
sb.AppendIndentedLine(indent, $"TypeName=`{tle.TypeName}`");
|
||||
}
|
||||
else if (ex is OutOfMemoryException)
|
||||
{
|
||||
@@ -78,14 +78,14 @@ namespace OpenRA
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
sb.AppendIndentedFormatLine(indent, $"GC Memory (post-collect)={GC.GetTotalMemory(false):N0}");
|
||||
sb.AppendIndentedFormatLine(indent, $"GC Memory (pre-collect)={gcMemoryBeforeCollect:N0}");
|
||||
sb.AppendIndentedLine(indent, $"GC Memory (post-collect)={GC.GetTotalMemory(false):N0}");
|
||||
sb.AppendIndentedLine(indent, $"GC Memory (pre-collect)={gcMemoryBeforeCollect:N0}");
|
||||
|
||||
using (var p = Process.GetCurrentProcess())
|
||||
{
|
||||
sb.AppendIndentedFormatLine(indent, $"Working Set={p.WorkingSet64:N0}");
|
||||
sb.AppendIndentedFormatLine(indent, $"Private Memory={p.PrivateMemorySize64:N0}");
|
||||
sb.AppendIndentedFormatLine(indent, $"Virtual Memory={p.VirtualMemorySize64:N0}");
|
||||
sb.AppendIndentedLine(indent, $"Working Set={p.WorkingSet64:N0}");
|
||||
sb.AppendIndentedLine(indent, $"Private Memory={p.PrivateMemorySize64:N0}");
|
||||
sb.AppendIndentedLine(indent, $"Virtual Memory={p.VirtualMemorySize64:N0}");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -95,11 +95,11 @@ namespace OpenRA
|
||||
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
sb.AppendIndentedFormatLine(indent, "Inner");
|
||||
sb.AppendIndentedLine(indent, "Inner");
|
||||
BuildExceptionReport(ex.InnerException, sb, indent + 1);
|
||||
}
|
||||
|
||||
sb.AppendIndentedFormatLine(indent, ex.StackTrace);
|
||||
sb.AppendIndentedLine(indent, ex.StackTrace);
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
@@ -65,15 +65,12 @@ namespace OpenRA.Mods.Common.Activities
|
||||
|
||||
public override bool Tick(Actor self)
|
||||
{
|
||||
if (cargo != carryall.Carryable)
|
||||
return true;
|
||||
|
||||
if (IsCanceling)
|
||||
if (IsCanceling || cargo != carryall.Carryable)
|
||||
{
|
||||
if (carryall.State == Carryall.CarryallState.Reserved)
|
||||
carryall.UnreserveCarryable(self);
|
||||
|
||||
// Make sure we run the TakeOff activity if we are / have landed
|
||||
// Make sure we run the TakeOff activity if we are/have landed
|
||||
if (self.Trait<Aircraft>().HasInfluence())
|
||||
{
|
||||
ChildHasPriority = true;
|
||||
@@ -88,7 +85,8 @@ namespace OpenRA.Mods.Common.Activities
|
||||
if (cargo.IsDead || carryable.IsTraitDisabled || !cargo.AppearsFriendlyTo(self))
|
||||
{
|
||||
carryall.UnreserveCarryable(self);
|
||||
return true;
|
||||
Cancel(self, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait until we are near the target before we try to lock it
|
||||
@@ -100,7 +98,10 @@ namespace OpenRA.Mods.Common.Activities
|
||||
{
|
||||
var lockResponse = carryable.LockForPickup(cargo, self);
|
||||
if (lockResponse == LockResponse.Failed)
|
||||
Cancel(self);
|
||||
{
|
||||
Cancel(self, true);
|
||||
return false;
|
||||
}
|
||||
else if (lockResponse == LockResponse.Success)
|
||||
{
|
||||
// Pickup position and facing are now known - swap the fly/wait activity with Land
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace OpenRA.Mods.Common.Graphics
|
||||
for (var i = 1; i < renderLength; i++)
|
||||
{
|
||||
var j = next - skip - 1 - i;
|
||||
var nextColor = Exts.ColorLerp(i * 1f / (renderLength - 1), startcolor, endcolor);
|
||||
var nextColor = Exts.ColorLerp(i / (renderLength - 1f), startcolor, endcolor);
|
||||
|
||||
var nextX = 0L;
|
||||
var nextY = 0L;
|
||||
|
||||
@@ -32,6 +32,15 @@ namespace OpenRA.Mods.Common.Lint
|
||||
var language = "en";
|
||||
var translation = new Translation(language, modData.Manifest.Translations, modData.DefaultFileSystem);
|
||||
|
||||
var gameSpeeds = modData.Manifest.Get<GameSpeeds>();
|
||||
foreach (var speed in gameSpeeds.Speeds.Values)
|
||||
{
|
||||
if (!translation.HasMessage(speed.Name))
|
||||
emitError($"{speed.Name} not present in {language} translation.");
|
||||
|
||||
referencedKeys.Add(speed.Name);
|
||||
}
|
||||
|
||||
foreach (var modType in modData.ObjectCreator.GetTypes())
|
||||
{
|
||||
foreach (var fieldInfo in modType.GetFields(Binding).Where(m => m.HasAttribute<TranslationReferenceAttribute>()))
|
||||
|
||||
@@ -281,6 +281,10 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
// When we build the cost table, it depends on the movement costs of the cells at that time.
|
||||
// When this changes, we must update the cost table.
|
||||
locomotor.CellCostChanged += RequireCostRefreshInCell;
|
||||
|
||||
// If the map projection changes, the result of Map.Contains(CPos) may change.
|
||||
// We need to rebuild grids to account for this possibility.
|
||||
this.world.Map.CellProjectionChanged += RequireProjectionRefreshInCell;
|
||||
}
|
||||
|
||||
public (
|
||||
@@ -619,6 +623,14 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When map projection changes for a cell, marks the grid it belongs to as out of date.
|
||||
/// </summary>
|
||||
void RequireProjectionRefreshInCell(CPos cell)
|
||||
{
|
||||
dirtyGridIndexes.Add(GridIndex(cell));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="BlockedByActor.Immovable"/> defines immovability based on the mobile trait. The blocking rules
|
||||
/// in <see cref="Locomotor.CanMoveFreelyInto(Actor, CPos, SubCell, BlockedByActor, Actor)"/> allow units to
|
||||
@@ -780,6 +792,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
fullGraph.GetConnections, locomotor, target, target, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self)))
|
||||
{
|
||||
var sourcesWithPathableNodes = new HashSet<CPos>(sources.Count);
|
||||
List<CPos> unpathableNodes = null;
|
||||
foreach (var (source, adjacentSource) in sourcesWithReachableNodes)
|
||||
{
|
||||
// Check if we have already found a route to this node before we attempt to expand the search.
|
||||
@@ -788,12 +801,24 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
if (sourceStatus.CostSoFar != PathGraph.PathCostForInvalidPath)
|
||||
sourcesWithPathableNodes.Add(source);
|
||||
else
|
||||
{
|
||||
if (unpathableNodes == null)
|
||||
unpathableNodes = new List<CPos>();
|
||||
unpathableNodes.Add(adjacentSource);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reverseAbstractSearch.TargetPredicate = cell => cell == adjacentSource;
|
||||
if (reverseAbstractSearch.ExpandToTarget())
|
||||
sourcesWithPathableNodes.Add(source);
|
||||
else
|
||||
{
|
||||
if (unpathableNodes == null)
|
||||
unpathableNodes = new List<CPos>();
|
||||
unpathableNodes.Add(adjacentSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,7 +827,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
|
||||
using (var fromSrc = GetLocalPathSearch(
|
||||
self, sourcesWithPathableNodes, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
|
||||
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize, sourcesWithPathableNodes),
|
||||
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize, sourcesWithPathableNodes, unpathableNodes),
|
||||
recorder: pathFinderOverlay?.RecordLocalEdges(self)))
|
||||
return fromSrc.FindPath();
|
||||
}
|
||||
@@ -852,12 +877,12 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
|
||||
RebuildDirtyGrids();
|
||||
|
||||
// If the target cell in unreachable, there is no path.
|
||||
// If the target cell is unreachable, there is no path.
|
||||
var targetAbstractCell = AbstractCellForLocalCell(target);
|
||||
if (targetAbstractCell == null)
|
||||
return PathFinder.NoPath;
|
||||
|
||||
// If the source cell in unreachable, there may still be a path.
|
||||
// If the source cell is unreachable, there may still be a path.
|
||||
// As long as one of the cells adjacent to the source is reachable, the path can be made.
|
||||
// Call the other overload which can handle this scenario.
|
||||
var sourceAbstractCell = AbstractCellForLocalCell(source);
|
||||
@@ -886,11 +911,11 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
|
||||
using (var fromSrc = GetLocalPathSearch(
|
||||
self, new[] { source }, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
|
||||
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize, null),
|
||||
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize, null, null),
|
||||
recorder: pathFinderOverlay?.RecordLocalEdges(self)))
|
||||
using (var fromDest = GetLocalPathSearch(
|
||||
self, new[] { target }, source, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
|
||||
heuristic: Heuristic(forwardAbstractSearch, estimatedSearchSize, null),
|
||||
heuristic: Heuristic(forwardAbstractSearch, estimatedSearchSize, null, null),
|
||||
recorder: pathFinderOverlay?.RecordLocalEdges(self),
|
||||
inReverse: true))
|
||||
return PathSearch.FindBidiPath(fromDest, fromSrc);
|
||||
@@ -1096,13 +1121,20 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
/// (the heuristic) for a local path search. The abstract search must run in the opposite direction to the
|
||||
/// local search. So when searching from source to target, the abstract search must be from target to source.
|
||||
/// </summary>
|
||||
Func<CPos, int> Heuristic(PathSearch abstractSearch, int estimatedSearchSize, HashSet<CPos> sources)
|
||||
Func<CPos, int> Heuristic(PathSearch abstractSearch, int estimatedSearchSize,
|
||||
HashSet<CPos> sources, List<CPos> unpathableNodes)
|
||||
{
|
||||
var nodeForCostLookup = new Dictionary<CPos, CPos>(estimatedSearchSize);
|
||||
var graph = (SparsePathGraph)abstractSearch.Graph;
|
||||
return cell =>
|
||||
{
|
||||
// All cells searched by the heuristic are guaranteed to be reachable.
|
||||
// When dealing with an unreachable source cell, the path search will check adjacent locations.
|
||||
// These cells may be reachable, but may represent jumping into an area cut off from the target.
|
||||
// Searching on the abstract graph would fail to provide a route in this scenario, so bail early.
|
||||
if (unpathableNodes != null && unpathableNodes.Contains(cell))
|
||||
return PathGraph.PathCostForInvalidPath;
|
||||
|
||||
// All other cells searched by the heuristic are guaranteed to be reachable.
|
||||
// So we don't need to handle an abstract cell lookup failing, or the search failing to expand.
|
||||
// Cells added as initial starting points for the search are filtered out if they aren't reachable.
|
||||
// The search only explores accessible cells from then on.
|
||||
@@ -1112,13 +1144,14 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
var maybeAbstractCell = AbstractCellForLocalCellNoAccessibleCheck(cell);
|
||||
if (maybeAbstractCell == null)
|
||||
{
|
||||
// If the source cell in unreachable, use one of the adjacent reachable cells instead.
|
||||
// If the source cell is unreachable, use one of the adjacent reachable cells instead.
|
||||
if (sources != null && sources.Contains(cell))
|
||||
{
|
||||
foreach (var dir in CVec.Directions)
|
||||
{
|
||||
var adjacentSource = cell + dir;
|
||||
if (!world.Map.Contains(adjacentSource))
|
||||
if (!world.Map.Contains(adjacentSource) ||
|
||||
(unpathableNodes != null && unpathableNodes.Contains(adjacentSource)))
|
||||
continue;
|
||||
|
||||
// Ideally we'd choose the cheapest cell rather than just any one of them,
|
||||
@@ -1132,7 +1165,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
if (maybeAbstractCell == null)
|
||||
throw new Exception(
|
||||
"The abstract path should never be searched for an unreachable point. " +
|
||||
"This is a bug. Failed lookup for an abstract cell.");
|
||||
$"Cell {cell} failed lookup for an abstract cell.");
|
||||
}
|
||||
|
||||
var abstractCell = maybeAbstractCell.Value;
|
||||
@@ -1145,7 +1178,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
if (!abstractSearch.ExpandToTarget())
|
||||
throw new Exception(
|
||||
"The abstract path should never be searched for an unreachable point. " +
|
||||
"This is a bug. Failed to route to abstract cell.");
|
||||
$"Abstract cell {abstractCell} failed to route to abstract cell.");
|
||||
info = graph[abstractCell];
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,11 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
return;
|
||||
}
|
||||
|
||||
var estimatedCost = heuristic(location) * heuristicWeightPercentage / 100;
|
||||
var heuristicCost = heuristic(location);
|
||||
if (heuristicCost == PathGraph.PathCostForInvalidPath)
|
||||
return;
|
||||
|
||||
var estimatedCost = heuristicCost * heuristicWeightPercentage / 100;
|
||||
Graph[location] = new CellInfo(CellStatus.Open, initialCost, initialCost + estimatedCost, location);
|
||||
var connection = new GraphConnection(location, estimatedCost);
|
||||
openQueue.Add(connection);
|
||||
@@ -237,14 +241,22 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
(neighborInfo.Status == CellStatus.Open && costSoFarToNeighbor >= neighborInfo.CostSoFar))
|
||||
continue;
|
||||
|
||||
// Now we may seriously consider this direction using heuristics. If the cell has
|
||||
// already been processed, we can reuse the result (just the difference between the
|
||||
// estimated total and the cost so far)
|
||||
// Now we may seriously consider this direction using heuristics.
|
||||
int estimatedRemainingCostToTarget;
|
||||
if (neighborInfo.Status == CellStatus.Open)
|
||||
{
|
||||
// If the cell has already been processed, we can reuse the result
|
||||
// (just the difference between the estimated total and the cost so far)
|
||||
estimatedRemainingCostToTarget = neighborInfo.EstimatedTotalCost - neighborInfo.CostSoFar;
|
||||
}
|
||||
else
|
||||
estimatedRemainingCostToTarget = heuristic(neighbor) * heuristicWeightPercentage / 100;
|
||||
{
|
||||
// If the heuristic reports the cell is unreachable, we won't consider it.
|
||||
var heuristicCost = heuristic(neighbor);
|
||||
if (heuristicCost == PathGraph.PathCostForInvalidPath)
|
||||
continue;
|
||||
estimatedRemainingCostToTarget = heuristicCost * heuristicWeightPercentage / 100;
|
||||
}
|
||||
|
||||
recorder?.Add(currentMinNode, neighbor, costSoFarToNeighbor, estimatedRemainingCostToTarget);
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace OpenRA.Mods.Common.Projectiles
|
||||
[Desc("How far beyond the target the projectile keeps on travelling.")]
|
||||
public readonly WDist BeyondTargetRange = new WDist(0);
|
||||
|
||||
[Desc("The minimum distance the beam travels.")]
|
||||
public readonly WDist MinDistance = WDist.Zero;
|
||||
|
||||
[Desc("Damage modifier applied at each range step.")]
|
||||
public readonly int[] Falloff = { 100, 100 };
|
||||
|
||||
@@ -136,7 +139,19 @@ namespace OpenRA.Mods.Common.Projectiles
|
||||
// Update the target position with the range we shoot beyond the target by
|
||||
// I.e. we can deliberately overshoot, so aim for that position
|
||||
var dir = new WVec(0, -1024, 0).Rotate(WRot.FromYaw(towardsTargetFacing));
|
||||
target += dir * info.BeyondTargetRange.Length / 1024;
|
||||
var dist = (args.SourceActor.CenterPosition - target).Length;
|
||||
int extraDist;
|
||||
if (info.MinDistance.Length > dist)
|
||||
{
|
||||
if (info.MinDistance.Length - dist < info.BeyondTargetRange.Length)
|
||||
extraDist = info.BeyondTargetRange.Length;
|
||||
else
|
||||
extraDist = info.MinDistance.Length - dist;
|
||||
}
|
||||
else
|
||||
extraDist = info.BeyondTargetRange.Length;
|
||||
|
||||
target += dir * extraDist / 1024;
|
||||
|
||||
length = Math.Max((target - headPos).Length / speed.Length, 1);
|
||||
weaponRange = new WDist(Util.ApplyPercentageModifiers(args.Weapon.Range.Length, args.RangeModifiers));
|
||||
|
||||
@@ -197,7 +197,7 @@ namespace OpenRA.Mods.Common.Projectiles
|
||||
if (info.ContrailLength > 0)
|
||||
{
|
||||
var startcolor = info.ContrailStartColorUsePlayerColor ? Color.FromArgb(info.ContrailStartColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailStartColorAlpha, info.ContrailStartColor);
|
||||
var endcolor = info.ContrailEndColorUsePlayerColor ? Color.FromArgb(info.ContrailEndColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailEndColorAlpha, info.ContrailEndColor ?? info.ContrailStartColor);
|
||||
var endcolor = info.ContrailEndColorUsePlayerColor ? Color.FromArgb(info.ContrailEndColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailEndColorAlpha, info.ContrailEndColor ?? startcolor);
|
||||
contrail = new ContrailRenderable(world, startcolor, endcolor, info.ContrailWidth, info.ContrailLength, info.ContrailDelay, info.ContrailZOffset);
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ namespace OpenRA.Mods.Common.Projectiles
|
||||
if (info.ContrailLength > 0)
|
||||
{
|
||||
var startcolor = info.ContrailStartColorUsePlayerColor ? Color.FromArgb(info.ContrailStartColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailStartColorAlpha, info.ContrailStartColor);
|
||||
var endcolor = info.ContrailEndColorUsePlayerColor ? Color.FromArgb(info.ContrailEndColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailEndColorAlpha, info.ContrailEndColor ?? info.ContrailStartColor);
|
||||
var endcolor = info.ContrailEndColorUsePlayerColor ? Color.FromArgb(info.ContrailEndColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailEndColorAlpha, info.ContrailEndColor ?? startcolor);
|
||||
contrail = new ContrailRenderable(world, startcolor, endcolor, info.ContrailWidth, info.ContrailLength, info.ContrailDelay, info.ContrailZOffset);
|
||||
}
|
||||
|
||||
|
||||
@@ -868,8 +868,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
public void AddInfluence((CPos, SubCell)[] landingCells)
|
||||
{
|
||||
if (HasInfluence())
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot {nameof(AddInfluence)} until previous influence is removed with {nameof(RemoveInfluence)}");
|
||||
self.World.ActorMap.RemoveInfluence(self, this);
|
||||
|
||||
this.landingCells = landingCells;
|
||||
if (self.IsInWorld)
|
||||
|
||||
@@ -106,30 +106,33 @@ namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
readonly Actor cargo;
|
||||
readonly Carryable carryable;
|
||||
readonly CarryallInfo carryallInfo;
|
||||
readonly Carryall carryall;
|
||||
|
||||
public FerryUnit(Actor self, Actor cargo)
|
||||
{
|
||||
this.cargo = cargo;
|
||||
carryable = cargo.Trait<Carryable>();
|
||||
carryallInfo = self.Trait<Carryall>().Info;
|
||||
carryall = self.Trait<Carryall>();
|
||||
}
|
||||
|
||||
protected override void OnFirstRun(Actor self)
|
||||
{
|
||||
if (!cargo.IsDead)
|
||||
QueueChild(new PickupUnit(self, cargo, 0, carryallInfo.TargetLineColor));
|
||||
QueueChild(new PickupUnit(self, cargo, 0, carryall.Info.TargetLineColor));
|
||||
}
|
||||
|
||||
public override bool Tick(Actor self)
|
||||
{
|
||||
if (cargo.IsDead)
|
||||
{
|
||||
carryall.UnreserveCarryable(self);
|
||||
return true;
|
||||
}
|
||||
|
||||
var dropRange = carryallInfo.DropRange;
|
||||
var dropRange = carryall.Info.DropRange;
|
||||
var destination = carryable.Destination;
|
||||
if (destination != null)
|
||||
self.QueueActivity(true, new DeliverUnit(self, Target.FromCell(self.World, destination.Value), dropRange, carryallInfo.TargetLineColor));
|
||||
self.QueueActivity(true, new DeliverUnit(self, Target.FromCell(self.World, destination.Value), dropRange, carryall.Info.TargetLineColor));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
|
||||
// Find new harvesters
|
||||
// TODO: Look for a more performance-friendly way to update this list
|
||||
var newHarvesters = world.ActorsHavingTrait<Harvester>().Where(a => a.Owner == player && !harvesters.ContainsKey(a));
|
||||
var newHarvesters = world.ActorsHavingTrait<Harvester>().Where(a => !unitCannotBeOrdered(a) && !harvesters.ContainsKey(a));
|
||||
foreach (var a in newHarvesters)
|
||||
harvesters[a] = new HarvesterTraitWrapper(a);
|
||||
|
||||
|
||||
@@ -256,7 +256,11 @@ namespace OpenRA.Mods.Common.Traits
|
||||
public virtual void UnreserveCarryable(Actor self)
|
||||
{
|
||||
if (Carryable != null && Carryable.IsInWorld && !Carryable.IsDead)
|
||||
Carryable.Trait<Carryable>().UnReserve(Carryable);
|
||||
{
|
||||
var carryable = Carryable.Trait<Carryable>();
|
||||
if (carryable.Carrier == self)
|
||||
carryable.UnReserve(Carryable);
|
||||
}
|
||||
|
||||
Carryable = null;
|
||||
State = CarryallState.Idle;
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
this.info = info;
|
||||
|
||||
startcolor = info.StartColorUsePlayerColor ? Color.FromArgb(info.StartColorAlpha, self.Owner.Color) : Color.FromArgb(info.StartColorAlpha, info.StartColor);
|
||||
endcolor = info.EndColorUsePlayerColor ? Color.FromArgb(info.EndColorAlpha, self.Owner.Color) : Color.FromArgb(info.EndColorAlpha, info.EndColor ?? info.StartColor);
|
||||
endcolor = info.EndColorUsePlayerColor ? Color.FromArgb(info.EndColorAlpha, self.Owner.Color) : Color.FromArgb(info.EndColorAlpha, info.EndColor ?? startcolor);
|
||||
trail = new ContrailRenderable(self.World, startcolor, endcolor, info.TrailWidth, info.TrailLength, info.TrailDelay, info.ZOffset);
|
||||
|
||||
body = self.Trait<BodyOrientation>();
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
WorldRenderer worldRenderer;
|
||||
|
||||
public MapPlayers Players { get; private set; }
|
||||
PlayerReference worldOwner;
|
||||
|
||||
public EditorActorLayer(EditorActorLayerInfo info)
|
||||
{
|
||||
@@ -59,7 +60,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
|
||||
Players = new MapPlayers(w.Map.PlayerDefinitions);
|
||||
|
||||
var worldOwner = Players.Players.Select(kvp => kvp.Value).First(p => !p.Playable && p.OwnsWorld);
|
||||
worldOwner = Players.Players.Select(kvp => kvp.Value).First(p => !p.Playable && p.OwnsWorld);
|
||||
w.SetWorldOwner(new Player(w, null, worldOwner, playerRandom));
|
||||
}
|
||||
|
||||
@@ -126,11 +127,17 @@ namespace OpenRA.Mods.Common.Traits
|
||||
|
||||
public EditorActorPreview Add(string id, ActorReference reference, bool initialSetup = false)
|
||||
{
|
||||
var owner = Players.Players[reference.Get<OwnerInit>().InternalName];
|
||||
// If an actor's doesn't have a valid owner transfer ownership to neutral
|
||||
var ownerInit = reference.Get<OwnerInit>();
|
||||
if (!Players.Players.TryGetValue(ownerInit.InternalName, out var owner))
|
||||
{
|
||||
owner = worldOwner;
|
||||
reference.Remove(ownerInit);
|
||||
reference.Add(new OwnerInit(worldOwner.Name));
|
||||
}
|
||||
|
||||
var preview = new EditorActorPreview(worldRenderer, id, reference, owner);
|
||||
|
||||
Add(preview, initialSetup);
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,10 +35,13 @@ namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
var actorReference = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
|
||||
|
||||
// If there is no real player associated, don't spawn it.
|
||||
var ownerName = actorReference.Get<OwnerInit>().InternalName;
|
||||
if (!world.Players.Any(p => p.InternalName == ownerName))
|
||||
continue;
|
||||
// If an actor's doesn't have a valid owner transfer ownership to neutral
|
||||
var ownerInit = actorReference.Get<OwnerInit>();
|
||||
if (!world.Players.Any(p => p.InternalName == ownerInit.InstanceName))
|
||||
{
|
||||
actorReference.Remove(ownerInit);
|
||||
actorReference.Add(new OwnerInit(world.WorldActor.Owner));
|
||||
}
|
||||
|
||||
actorReference.Add(new SkipMakeAnimsInit());
|
||||
actorReference.Add(new SpawnedByMapInit(kv.Key));
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace OpenRA.Mods.Common.Widgets
|
||||
|
||||
// Equivalent to OnMouseUp, but without an input arg
|
||||
public Action OnClick = () => { };
|
||||
public Action OnDoubleClick = () => { };
|
||||
public Action OnDoubleClick = null;
|
||||
public Action<KeyInput> OnKeyPress = _ => { };
|
||||
|
||||
public string Cursor = ChromeMetrics.Get<string>("ButtonCursor");
|
||||
@@ -163,7 +163,7 @@ namespace OpenRA.Mods.Common.Widgets
|
||||
return false;
|
||||
|
||||
var disabled = IsDisabled();
|
||||
if (HasMouseFocus && mi.Event == MouseInputEvent.Up && mi.MultiTapCount == 2)
|
||||
if (HasMouseFocus && mi.Event == MouseInputEvent.Up && mi.MultiTapCount == 2 && OnDoubleClick != null)
|
||||
{
|
||||
if (!disabled)
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
readonly BackgroundWidget actorEditPanel;
|
||||
readonly LabelWidget typeLabel;
|
||||
readonly TextFieldWidget actorIDField;
|
||||
readonly HashSet<TextFieldWidget> typableFields = new HashSet<TextFieldWidget>();
|
||||
readonly LabelWidget actorIDErrorLabel;
|
||||
|
||||
readonly Widget initContainer;
|
||||
@@ -328,6 +329,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
if (float.TryParse(valueField.Text, out var result))
|
||||
slider.UpdateValue(result);
|
||||
};
|
||||
|
||||
valueField.OnEscKey = _ => { valueField.YieldKeyboardFocus(); return true; };
|
||||
valueField.OnEnterKey = _ => { valueField.YieldKeyboardFocus(); return true; };
|
||||
typableFields.Add(valueField);
|
||||
}
|
||||
|
||||
initContainer.AddChild(sliderContainer);
|
||||
@@ -372,11 +377,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
actorEditPanel.Bounds.X = origin.X + editPanelPadding;
|
||||
actorEditPanel.Bounds.Y = origin.Y;
|
||||
}
|
||||
else
|
||||
else if (CurrentActor != null)
|
||||
{
|
||||
// Selected actor is null, hide the border and edit panel.
|
||||
actorIDField.YieldKeyboardFocus();
|
||||
CurrentActor = null;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +406,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
void Close()
|
||||
{
|
||||
actorIDField.YieldKeyboardFocus();
|
||||
foreach (var f in typableFields)
|
||||
f.YieldKeyboardFocus();
|
||||
|
||||
editor.DefaultBrush.SelectedActor = null;
|
||||
CurrentActor = null;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Traits;
|
||||
using OpenRA.Widgets;
|
||||
@@ -25,41 +26,27 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
|
||||
var visibilityCheckbox = widget.GetOrNull<CheckboxWidget>("DISABLE_VISIBILITY_CHECKS");
|
||||
if (visibilityCheckbox != null)
|
||||
{
|
||||
visibilityCheckbox.IsChecked = () => devTrait.DisableShroud;
|
||||
visibilityCheckbox.OnClick = () => Order(world, "DevVisibility");
|
||||
}
|
||||
BindOrderCheckbox(visibilityCheckbox, world, "DevVisibility", () => devTrait.DisableShroud);
|
||||
|
||||
var pathCheckbox = widget.GetOrNull<CheckboxWidget>("SHOW_UNIT_PATHS");
|
||||
if (pathCheckbox != null)
|
||||
{
|
||||
pathCheckbox.IsChecked = () => devTrait.PathDebug;
|
||||
pathCheckbox.OnClick = () => Order(world, "DevPathDebug");
|
||||
}
|
||||
BindOrderCheckbox(pathCheckbox, world, "DevPathDebug", () => devTrait.PathDebug);
|
||||
|
||||
var cashButton = widget.GetOrNull<ButtonWidget>("GIVE_CASH");
|
||||
if (cashButton != null)
|
||||
cashButton.OnClick = () =>
|
||||
world.IssueOrder(new Order("DevGiveCash", world.LocalPlayer.PlayerActor, false));
|
||||
cashButton.OnClick = () => IssueOrder(world, "DevGiveCash");
|
||||
|
||||
var growResourcesButton = widget.GetOrNull<ButtonWidget>("GROW_RESOURCES");
|
||||
if (growResourcesButton != null)
|
||||
growResourcesButton.OnClick = () =>
|
||||
world.IssueOrder(new Order("DevGrowResources", world.LocalPlayer.PlayerActor, false));
|
||||
growResourcesButton.OnClick = () => IssueOrder(world, "DevGrowResources");
|
||||
|
||||
var fastBuildCheckbox = widget.GetOrNull<CheckboxWidget>("INSTANT_BUILD");
|
||||
if (fastBuildCheckbox != null)
|
||||
{
|
||||
fastBuildCheckbox.IsChecked = () => devTrait.FastBuild;
|
||||
fastBuildCheckbox.OnClick = () => Order(world, "DevFastBuild");
|
||||
}
|
||||
BindOrderCheckbox(fastBuildCheckbox, world, "DevFastBuild", () => devTrait.FastBuild);
|
||||
|
||||
var fastChargeCheckbox = widget.GetOrNull<CheckboxWidget>("INSTANT_CHARGE");
|
||||
if (fastChargeCheckbox != null)
|
||||
{
|
||||
fastChargeCheckbox.IsChecked = () => devTrait.FastCharge;
|
||||
fastChargeCheckbox.OnClick = () => Order(world, "DevFastCharge");
|
||||
}
|
||||
BindOrderCheckbox(fastChargeCheckbox, world, "DevFastCharge", () => devTrait.FastCharge);
|
||||
|
||||
var showCombatCheckbox = widget.GetOrNull<CheckboxWidget>("SHOW_COMBATOVERLAY");
|
||||
if (showCombatCheckbox != null)
|
||||
@@ -103,34 +90,23 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
|
||||
var allTechCheckbox = widget.GetOrNull<CheckboxWidget>("ENABLE_TECH");
|
||||
if (allTechCheckbox != null)
|
||||
{
|
||||
allTechCheckbox.IsChecked = () => devTrait.AllTech;
|
||||
allTechCheckbox.OnClick = () => Order(world, "DevEnableTech");
|
||||
}
|
||||
BindOrderCheckbox(allTechCheckbox, world, "DevEnableTech", () => devTrait.AllTech);
|
||||
|
||||
var powerCheckbox = widget.GetOrNull<CheckboxWidget>("UNLIMITED_POWER");
|
||||
if (powerCheckbox != null)
|
||||
{
|
||||
powerCheckbox.IsChecked = () => devTrait.UnlimitedPower;
|
||||
powerCheckbox.OnClick = () => Order(world, "DevUnlimitedPower");
|
||||
}
|
||||
BindOrderCheckbox(powerCheckbox, world, "DevUnlimitedPower", () => devTrait.UnlimitedPower);
|
||||
|
||||
var buildAnywhereCheckbox = widget.GetOrNull<CheckboxWidget>("BUILD_ANYWHERE");
|
||||
if (buildAnywhereCheckbox != null)
|
||||
{
|
||||
buildAnywhereCheckbox.IsChecked = () => devTrait.BuildAnywhere;
|
||||
buildAnywhereCheckbox.OnClick = () => Order(world, "DevBuildAnywhere");
|
||||
}
|
||||
BindOrderCheckbox(buildAnywhereCheckbox, world, "DevBuildAnywhere", () => devTrait.BuildAnywhere);
|
||||
|
||||
var explorationButton = widget.GetOrNull<ButtonWidget>("GIVE_EXPLORATION");
|
||||
if (explorationButton != null)
|
||||
explorationButton.OnClick = () =>
|
||||
world.IssueOrder(new Order("DevGiveExploration", world.LocalPlayer.PlayerActor, false));
|
||||
explorationButton.OnClick = () => IssueOrder(world, "DevGiveExploration");
|
||||
|
||||
var noexplorationButton = widget.GetOrNull<ButtonWidget>("RESET_EXPLORATION");
|
||||
if (noexplorationButton != null)
|
||||
noexplorationButton.OnClick = () =>
|
||||
world.IssueOrder(new Order("DevResetExploration", world.LocalPlayer.PlayerActor, false));
|
||||
noexplorationButton.OnClick = () => IssueOrder(world, "DevResetExploration");
|
||||
|
||||
var showActorTagsCheckbox = widget.GetOrNull<CheckboxWidget>("SHOW_ACTOR_TAGS");
|
||||
if (showActorTagsCheckbox != null)
|
||||
@@ -153,7 +129,18 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
}
|
||||
}
|
||||
|
||||
public static void Order(World world, string order)
|
||||
static void BindOrderCheckbox(CheckboxWidget checkbox, World world, string order, Func<bool> getValue)
|
||||
{
|
||||
var isChecked = new PredictedCachedTransform<bool, bool>(state => state);
|
||||
checkbox.IsChecked = () => isChecked.Update(getValue());
|
||||
checkbox.OnClick = () =>
|
||||
{
|
||||
isChecked.Predict(!getValue());
|
||||
IssueOrder(world, order);
|
||||
};
|
||||
}
|
||||
|
||||
public static void IssueOrder(World world, string order)
|
||||
{
|
||||
world.IssueOrder(new Order(order, world.LocalPlayer.PlayerActor, false));
|
||||
}
|
||||
|
||||
@@ -448,6 +448,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
var harvesters = template.Get<LabelWidget>("HARVESTERS");
|
||||
harvesters.GetText = () => world.ActorsWithTrait<Harvester>().Count(a => a.Actor.Owner == player && !a.Actor.IsDead && !a.Trait.IsTraitDisabled).ToString();
|
||||
|
||||
var carryalls = template.GetOrNull<LabelWidget>("CARRYALLS");
|
||||
if (carryalls != null)
|
||||
carryalls.GetText = () => world.ActorsWithTrait<AutoCarryall>().Count(a => a.Actor.Owner == player && !a.Actor.IsDead).ToString();
|
||||
|
||||
var derricks = template.GetOrNull<LabelWidget>("DERRICKS");
|
||||
if (derricks != null)
|
||||
derricks.GetText = () => world.ActorsHavingTrait<UpdatesDerrickCount>().Count(a => a.Owner == player && !a.IsDead).ToString();
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
var dataTotal = 0.0f;
|
||||
var mag = 0;
|
||||
var dataSuffix = "";
|
||||
var host = downloadHost ?? UnknownHost;
|
||||
var host = downloadHost ?? modData.Translation.GetString(UnknownHost);
|
||||
|
||||
if (total < 0)
|
||||
{
|
||||
@@ -139,11 +139,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
|
||||
Action<string> onError = s => Game.RunAfterTick(() =>
|
||||
{
|
||||
Log.Write("install", "Download failed: " + s);
|
||||
var host = downloadHost ?? modData.Translation.GetString(UnknownHost);
|
||||
Log.Write("install", $"Download from {host} failed: " + s);
|
||||
|
||||
progressBar.Indeterminate = false;
|
||||
progressBar.Percentage = 100;
|
||||
getStatusText = () => "Error: " + s;
|
||||
getStatusText = () => $"{host}: Error: {s}";
|
||||
retryButton.IsVisible = () => true;
|
||||
cancelButton.OnClick = Ui.CloseWindow;
|
||||
});
|
||||
|
||||
@@ -101,18 +101,25 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
}
|
||||
|
||||
var checkbox = checkboxColumns.Dequeue();
|
||||
var optionValue = new CachedTransform<Session.Global, Session.LobbyOptionState>(
|
||||
gs => gs.LobbyOptions[option.Id]);
|
||||
var optionEnabled = new PredictedCachedTransform<Session.Global, bool>(
|
||||
gs => gs.LobbyOptions[option.Id].IsEnabled);
|
||||
|
||||
var optionLocked = new CachedTransform<Session.Global, bool>(
|
||||
gs => gs.LobbyOptions[option.Id].IsLocked);
|
||||
|
||||
checkbox.GetText = () => option.Name;
|
||||
if (option.Description != null)
|
||||
checkbox.GetTooltipText = () => option.Description;
|
||||
|
||||
checkbox.IsVisible = () => true;
|
||||
checkbox.IsChecked = () => optionValue.Update(orderManager.LobbyInfo.GlobalSettings).IsEnabled;
|
||||
checkbox.IsDisabled = () => configurationDisabled() || optionValue.Update(orderManager.LobbyInfo.GlobalSettings).IsLocked;
|
||||
checkbox.OnClick = () => orderManager.IssueOrder(Order.Command(
|
||||
$"option {option.Id} {!optionValue.Update(orderManager.LobbyInfo.GlobalSettings).IsEnabled}"));
|
||||
checkbox.IsChecked = () => optionEnabled.Update(orderManager.LobbyInfo.GlobalSettings);
|
||||
checkbox.IsDisabled = () => configurationDisabled() || optionLocked.Update(orderManager.LobbyInfo.GlobalSettings);
|
||||
checkbox.OnClick = () =>
|
||||
{
|
||||
var state = !optionEnabled.Update(orderManager.LobbyInfo.GlobalSettings);
|
||||
orderManager.IssueOrder(Order.Command($"option {option.Id} {state}"));
|
||||
optionEnabled.Predict(state);
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var option in allOptions.Where(o => !(o is LobbyBooleanOption)))
|
||||
|
||||
@@ -657,12 +657,21 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
public static void SetupEditableReadyWidget(Widget parent, Session.Client c, OrderManager orderManager, MapPreview map, bool isEnabled)
|
||||
{
|
||||
var status = parent.Get<CheckboxWidget>("STATUS_CHECKBOX");
|
||||
status.IsChecked = () => orderManager.LocalClient.IsReady || c.Bot != null;
|
||||
status.IsVisible = () => true;
|
||||
status.IsDisabled = () => c.Bot != null || map.Status != MapStatus.Available || !isEnabled;
|
||||
|
||||
var state = orderManager.LocalClient.IsReady ? Session.ClientState.NotReady : Session.ClientState.Ready;
|
||||
status.OnClick = () => orderManager.IssueOrder(Order.Command($"state {state}"));
|
||||
if (c.Bot == null)
|
||||
{
|
||||
var isChecked = new PredictedCachedTransform<Session.Client, bool>(cc => cc.IsReady);
|
||||
status.IsChecked = () => isChecked.Update(c);
|
||||
status.OnClick = () =>
|
||||
{
|
||||
var state = isChecked.Update(c) ? Session.ClientState.NotReady : Session.ClientState.Ready;
|
||||
orderManager.IssueOrder(Order.Command($"state {state}"));
|
||||
isChecked.Predict(!c.IsReady);
|
||||
};
|
||||
}
|
||||
else
|
||||
status.IsChecked = () => true;
|
||||
}
|
||||
|
||||
public static void SetupReadyWidget(Widget parent, Session.Client c)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
public class SystemInfoPromptLogic : ChromeLogic
|
||||
{
|
||||
// Increment the version number when adding new stats
|
||||
const int SystemInformationVersion = 4;
|
||||
const int SystemInformationVersion = 5;
|
||||
|
||||
static Dictionary<string, (string Label, string Value)> GetSystemInformation()
|
||||
{
|
||||
@@ -29,6 +29,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
{
|
||||
{ "id", ("Anonymous ID", Game.Settings.Debug.UUID) },
|
||||
{ "platform", ("OS Type", Platform.CurrentPlatform.ToString()) },
|
||||
{ "arch", ("Architecture", Platform.CurrentArchitecture.ToString()) },
|
||||
{ "os", ("OS Version", Environment.OSVersion.ToString()) },
|
||||
{ "x64", ("OS is 64 bit", Environment.Is64BitOperatingSystem.ToString()) },
|
||||
{ "x64process", ("Process is 64 bit", Environment.Is64BitProcess.ToString()) },
|
||||
|
||||
@@ -419,4 +419,40 @@ namespace OpenRA.Mods.Common.Widgets
|
||||
return lastOutput;
|
||||
}
|
||||
}
|
||||
|
||||
public class PredictedCachedTransform<T, U>
|
||||
{
|
||||
readonly Func<T, U> transform;
|
||||
|
||||
bool initialized;
|
||||
T lastInput;
|
||||
U lastOutput;
|
||||
|
||||
bool predicted;
|
||||
U prediction;
|
||||
|
||||
public PredictedCachedTransform(Func<T, U> transform)
|
||||
{
|
||||
this.transform = transform;
|
||||
}
|
||||
|
||||
public void Predict(U value)
|
||||
{
|
||||
predicted = true;
|
||||
prediction = value;
|
||||
}
|
||||
|
||||
public U Update(T input)
|
||||
{
|
||||
if ((predicted || initialized) && ((input == null && lastInput == null) || (input != null && input.Equals(lastInput))))
|
||||
return predicted ? prediction : lastOutput;
|
||||
|
||||
predicted = false;
|
||||
initialized = true;
|
||||
lastInput = input;
|
||||
lastOutput = transform(input);
|
||||
|
||||
return lastOutput;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj" />
|
||||
<PackageReference Include="OpenRA-Freetype6" Version="1.0.4" />
|
||||
<PackageReference Include="OpenRA-OpenAL-CS" Version="1.0.16" />
|
||||
<PackageReference Include="OpenRA-SDL2-CS" Version="1.0.31" />
|
||||
<PackageReference Include="OpenRA-Freetype6" Version="1.0.9" />
|
||||
<PackageReference Include="OpenRA-OpenAL-CS" Version="1.0.17" />
|
||||
<PackageReference Include="OpenRA-SDL2-CS" Version="1.0.36" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="OpenRA.Platforms.Default.dll.config" Condition="'$(TargetPlatform)' != 'win-x64' And '$(TargetPlatform)' != 'win-x86'">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenRA-SDL2-CS" Version="1.0.31" />
|
||||
<PackageReference Include="OpenRA-SDL2-CS" Version="1.0.36" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj" />
|
||||
|
||||
@@ -1549,7 +1549,7 @@ Container@PLAYER_WIDGETS:
|
||||
X: WINDOW_RIGHT - WIDTH - 5
|
||||
Y: 5
|
||||
Width: 230
|
||||
Height: 291
|
||||
Height: 314
|
||||
ImageCollection: sidebar
|
||||
ImageName: background-sidebar
|
||||
ClickThrough: false
|
||||
|
||||
@@ -313,7 +313,7 @@ Container@OBSERVER_WIDGETS:
|
||||
Container@ECONOMY_STATS_HEADERS:
|
||||
X: 0
|
||||
Y: 0
|
||||
Width: 640
|
||||
Width: 720
|
||||
Height: PARENT_BOTTOM
|
||||
Children:
|
||||
ColorBlock@HEADER_COLOR:
|
||||
@@ -384,6 +384,14 @@ Container@OBSERVER_WIDGETS:
|
||||
Text: Harvesters
|
||||
Align: Right
|
||||
Shadow: True
|
||||
Label@CARRYALLS_HEADER:
|
||||
X: 635
|
||||
Width: 80
|
||||
Height: PARENT_BOTTOM
|
||||
Font: Bold
|
||||
Text: Carryalls
|
||||
Align: Right
|
||||
Shadow: True
|
||||
Container@PRODUCTION_STATS_HEADERS:
|
||||
X: 0
|
||||
Y: 0
|
||||
@@ -717,7 +725,7 @@ Container@OBSERVER_WIDGETS:
|
||||
ScrollItem@ECONOMY_PLAYER_TEMPLATE:
|
||||
X: 0
|
||||
Y: 0
|
||||
Width: 640
|
||||
Width: 720
|
||||
Height: 25
|
||||
Background: scrollitem-nohover
|
||||
Children:
|
||||
@@ -785,6 +793,13 @@ Container@OBSERVER_WIDGETS:
|
||||
Height: PARENT_BOTTOM
|
||||
Align: Right
|
||||
Shadow: True
|
||||
Label@CARRYALLS:
|
||||
X: 635
|
||||
Y: 0
|
||||
Width: 80
|
||||
Height: PARENT_BOTTOM
|
||||
Align: Right
|
||||
Shadow: True
|
||||
ScrollItem@PRODUCTION_PLAYER_TEMPLATE:
|
||||
X: 0
|
||||
Y: 0
|
||||
|
||||
BIN
mods/d2k/maps/side-step.oramap
Normal file
BIN
mods/d2k/maps/side-step.oramap
Normal file
Binary file not shown.
BIN
mods/d2k/maps/source.oramap
Normal file
BIN
mods/d2k/maps/source.oramap
Normal file
Binary file not shown.
BIN
mods/d2k/maps/spice-mesa.oramap
Normal file
BIN
mods/d2k/maps/spice-mesa.oramap
Normal file
Binary file not shown.
BIN
mods/d2k/maps/stone-plateaus.oramap
Normal file
BIN
mods/d2k/maps/stone-plateaus.oramap
Normal file
Binary file not shown.
BIN
mods/d2k/maps/sunstroke.oramap
Normal file
BIN
mods/d2k/maps/sunstroke.oramap
Normal file
Binary file not shown.
@@ -175,8 +175,8 @@ Templates:
|
||||
Size: 1,2
|
||||
Categories: Sand-Rock-Cliff
|
||||
Tiles:
|
||||
0: Transition
|
||||
1: Transition
|
||||
0: Rough
|
||||
1: Rough
|
||||
Template@10:
|
||||
Id: 10
|
||||
Images: BLOXBASE.R8
|
||||
|
||||
@@ -13,6 +13,7 @@ Sound:
|
||||
Falloff: 0, 0, 100, 0
|
||||
Range: 0, 0c450, 4c0, 8c0
|
||||
BeyondTargetRange: 1c0
|
||||
MinDistance: 5c0
|
||||
Color: 00FFFFC8
|
||||
Warhead@1Dam: SpreadDamage
|
||||
Range: 0, 32
|
||||
|
||||
@@ -603,7 +603,7 @@ Container@PLAYER_WIDGETS:
|
||||
X: WINDOW_RIGHT - 250
|
||||
Y: 272
|
||||
Width: 238
|
||||
Height: 27
|
||||
Height: 28
|
||||
ImageCollection: sidebar
|
||||
ImageName: background-moneybin
|
||||
ClickThrough: false
|
||||
|
||||
@@ -44,7 +44,7 @@ Tick = function()
|
||||
USSR.MarkCompletedObjective(SovietObj)
|
||||
end
|
||||
|
||||
if USSR.HasNoRequiredUnits() then
|
||||
if USSR.HasNoRequiredUnits() and BadGuy.HasNoRequiredUnits() then
|
||||
Allies.MarkCompletedObjective(DestroyAll)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,6 +27,9 @@ AFLD:
|
||||
ParatroopersPower@paratroopers:
|
||||
DropItems: E1,E1,E1,E2,E2
|
||||
|
||||
HELI:
|
||||
-MustBeDestroyed:
|
||||
|
||||
ATEK:
|
||||
Buildable:
|
||||
Prerequisites: ~disabled
|
||||
|
||||
@@ -27,6 +27,9 @@ AFLD:
|
||||
ParatroopersPower@paratroopers:
|
||||
DropItems: E1,E1,E1,E2,E2
|
||||
|
||||
HELI:
|
||||
-MustBeDestroyed:
|
||||
|
||||
ATEK:
|
||||
Buildable:
|
||||
Prerequisites: ~disabled
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# Arguments:
|
||||
# SRC_PATH: Path to the root OpenRA directory
|
||||
# DEST_PATH: Path to the root of the install destination (will be created if necessary)
|
||||
# TARGETPLATFORM: Platform type (win-x86, win-x64, osx-x64, linux-x64, unix-generic)
|
||||
# TARGETPLATFORM: Platform type (win-x86, win-x64, osx-x64, osx-arm64, linux-x64, linux-arm64, unix-generic)
|
||||
# RUNTIME: Runtime type (net6, mono)
|
||||
# COPY_GENERIC_LAUNCHER: If set to True the OpenRA.exe will also be copied (True, False)
|
||||
# COPY_CNC_DLL: If set to True the OpenRA.Mods.Cnc.dll will also be copied (True, False)
|
||||
@@ -68,13 +68,13 @@ install_assemblies() (
|
||||
install -m644 "${LIB}" "${DEST_PATH}"
|
||||
done
|
||||
|
||||
if [ "${TARGETPLATFORM}" = "linux-x64" ]; then
|
||||
if [ "${TARGETPLATFORM}" = "linux-x64" ] || [ "${TARGETPLATFORM}" = "linux-arm64" ]; then
|
||||
for LIB in "${SRC_PATH}/bin/"*.so; do
|
||||
install -m755 "${LIB}" "${DEST_PATH}"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "${TARGETPLATFORM}" = "osx-x64" ]; then
|
||||
if [ "${TARGETPLATFORM}" = "osx-x64" ] || [ "${TARGETPLATFORM}" = "osx-arm64" ]; then
|
||||
for LIB in "${SRC_PATH}/bin/"*.dylib; do
|
||||
install -m755 "${LIB}" "${DEST_PATH}"
|
||||
done
|
||||
|
||||
@@ -3,4 +3,10 @@ set -o errexit || exit $?
|
||||
|
||||
cd "{GAME_INSTALL_DIR}"
|
||||
|
||||
mono {DEBUG} OpenRA.Server.dll Game.Mod={MODID} "$@"
|
||||
if test -f "OpenRA.Server"; then
|
||||
./OpenRA.Server Game.Mod={MODID} "$@"
|
||||
elif command -v mono >/dev/null 2>&1 && [ "$(grep -c .NETCoreApp,Version= OpenRA.Server.dll)" = "0" ]; then
|
||||
mono {DEBUG} OpenRA.Server.dll Game.Mod={MODID} "$@"
|
||||
else
|
||||
dotnet OpenRA.Server.dll Game.Mod={MODID} "$@"
|
||||
fi
|
||||
|
||||
@@ -11,7 +11,13 @@ if [ "${1#${PROTOCOL_PREFIX}}" != "${1}" ]; then
|
||||
fi
|
||||
|
||||
# Run the game
|
||||
mono {DEBUG} OpenRA.dll Game.Mod={MODID} Engine.LaunchPath="{BIN_DIR}/openra-{MODID}" "${JOIN_SERVER}" "$@" && rc=0 || rc=$?
|
||||
if test -f "OpenRA"; then
|
||||
./OpenRA Game.Mod={MODID} Engine.LaunchPath="{BIN_DIR}/openra-{MODID}" "${JOIN_SERVER}" "$@" && rc=0 || rc=$?
|
||||
elif command -v mono >/dev/null 2>&1 && [ "$(grep -c .NETCoreApp,Version= OpenRA.dll)" = "0" ]; then
|
||||
mono {DEBUG} OpenRA.dll Game.Mod={MODID} Engine.LaunchPath="{BIN_DIR}/openra-{MODID}" "${JOIN_SERVER}" "$@" && rc=0 || rc=$?
|
||||
else
|
||||
dotnet OpenRA.dll Game.Mod={MODID} Engine.LaunchPath="{BIN_DIR}/openra-{MODID}" "${JOIN_SERVER}" "$@" && rc=0 || rc=$?
|
||||
fi
|
||||
|
||||
# Show a crash dialog if something went wrong
|
||||
if [ "${rc}" != 0 ] && [ "${rc}" != 1 ]; then
|
||||
|
||||
@@ -48,5 +48,7 @@
|
||||
<string>{JOIN_SERVER_URL_SCHEME}</string>
|
||||
<key>NSRequiresAquaSystemAppearance</key>
|
||||
<false/>
|
||||
<key>NSPrefersDisplaySafeAreaCompatibilityMode</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
48
packaging/macos/apphost-mono.c
Normal file
48
packaging/macos/apphost-mono.c
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
|
||||
//
|
||||
// A custom apphost is required (instead of just invoking `mono OpenRA.dll ...` directly)
|
||||
// because macOS will only properly associate dock icons and tooltips to windows that are
|
||||
// created by a process in the Contents/MacOS directory (not subdirectories).
|
||||
//
|
||||
// Based on https://github.com/mono/monodevelop/blob/main/main/build/MacOSX/monostub.mm
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/resource.h>
|
||||
|
||||
typedef int (* mono_main)(int argc, char **argv);
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// 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(argv[1], RTLD_LAZY);
|
||||
if (libmono == NULL)
|
||||
{
|
||||
fprintf(stderr, "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)
|
||||
{
|
||||
fprintf(stderr, "Could not load mono_main(): %s\n", dlerror());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return _mono_main(argc - 1, &argv[1]);
|
||||
}
|
||||
84
packaging/macos/apphost.c
Normal file
84
packaging/macos/apphost.c
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
|
||||
//
|
||||
// A custom apphost is required (instead of just invoking <arch-dir>/OpenRA directly)
|
||||
// because macOS will only properly associate dock icons and tooltips to windows that are
|
||||
// created by a process in the Contents/MacOS directory (not subdirectories).
|
||||
//
|
||||
// .NET 6 does not support universal binaries, and the apphost that is created when
|
||||
// publishing requires the runtime files to exist in the same directory as the launcher.
|
||||
//
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <libgen.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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);
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
void *lib = dlopen(argv[1], RTLD_LAZY);
|
||||
if (lib == NULL)
|
||||
{
|
||||
fprintf(stderr, "Failed to load %s: %s\n", argv[1], 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)
|
||||
{
|
||||
fprintf(stderr, "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)
|
||||
{
|
||||
fprintf(stderr, "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)
|
||||
{
|
||||
fprintf(stderr, "Could not load hostfxr_close(): %s\n", dlerror());
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct hostfxr_initialize_parameters params;
|
||||
params.size = sizeof(params);
|
||||
params.host_path = argv[0];
|
||||
params.dotnet_root = dirname(argv[1]);
|
||||
|
||||
hostfxr_handle host_context_handle;
|
||||
hostfxr_initialize_for_dotnet_command_line(
|
||||
argc - 2,
|
||||
&argv[2],
|
||||
¶ms,
|
||||
&host_context_handle);
|
||||
|
||||
hostfxr_run_app(host_context_handle);
|
||||
|
||||
return hostfxr_close(host_context_handle);
|
||||
}
|
||||
@@ -16,15 +16,15 @@
|
||||
|
||||
set -o errexit -o pipefail || exit $?
|
||||
|
||||
MONO_TAG="osx-launcher-20201222"
|
||||
|
||||
if [ $# -ne "2" ]; then
|
||||
echo "Usage: $(basename "$0") tag outputdir"
|
||||
if [[ "${OSTYPE}" != "darwin"* ]]; then
|
||||
echo >&2 "macOS packaging requires a macOS host"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${OSTYPE}" != "darwin"* ]]; then
|
||||
echo >&2 "macOS packaging requires a macOS host"
|
||||
command -v clang >/dev/null 2>&1 || { echo >&2 "macOS packaging requires clang."; exit 1; }
|
||||
|
||||
if [ $# -ne "2" ]; then
|
||||
echo "Usage: $(basename "$0") tag outputdir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -47,6 +47,7 @@ fi
|
||||
|
||||
TAG="${1}"
|
||||
OUTPUTDIR="${2}"
|
||||
|
||||
SRCDIR="$(pwd)/../.."
|
||||
BUILTDIR="$(pwd)/build"
|
||||
ARTWORK_DIR="$(pwd)/../artwork/"
|
||||
@@ -57,15 +58,13 @@ modify_plist() {
|
||||
|
||||
# Copies the game files and sets metadata
|
||||
build_app() {
|
||||
PLATFORM="${1}"
|
||||
TEMPLATE_DIR="${2}"
|
||||
LAUNCHER_DIR="${3}"
|
||||
MOD_ID="${4}"
|
||||
MOD_NAME="${5}"
|
||||
DISCORD_APPID="${6}"
|
||||
TEMPLATE_DIR="${1}"
|
||||
LAUNCHER_DIR="${2}"
|
||||
MOD_ID="${3}"
|
||||
MOD_NAME="${4}"
|
||||
DISCORD_APPID="${5}"
|
||||
|
||||
LAUNCHER_CONTENTS_DIR="${LAUNCHER_DIR}/Contents"
|
||||
LAUNCHER_ASSEMBLY_DIR="${LAUNCHER_CONTENTS_DIR}/MacOS"
|
||||
LAUNCHER_RESOURCES_DIR="${LAUNCHER_CONTENTS_DIR}/Resources"
|
||||
|
||||
cp -r "${TEMPLATE_DIR}" "${LAUNCHER_DIR}"
|
||||
@@ -76,12 +75,10 @@ build_app() {
|
||||
fi
|
||||
|
||||
# Install engine and mod files
|
||||
RUNTIME="net6"
|
||||
if [ "${PLATFORM}" = "mono" ]; then
|
||||
RUNTIME="mono"
|
||||
fi
|
||||
install_assemblies "${SRCDIR}" "${LAUNCHER_CONTENTS_DIR}/MacOS/x86_64" "osx-x64" "net6" "True" "True" "${IS_D2K}"
|
||||
install_assemblies "${SRCDIR}" "${LAUNCHER_CONTENTS_DIR}/MacOS/arm64" "osx-arm64" "net6" "True" "True" "${IS_D2K}"
|
||||
install_assemblies "${SRCDIR}" "${LAUNCHER_CONTENTS_DIR}/MacOS/mono" "osx-x64" "mono" "True" "True" "${IS_D2K}"
|
||||
|
||||
install_assemblies "${SRCDIR}" "${LAUNCHER_ASSEMBLY_DIR}" "osx-x64" "${RUNTIME}" "True" "True" "${IS_D2K}"
|
||||
install_data "${SRCDIR}" "${LAUNCHER_RESOURCES_DIR}" "${MOD_ID}"
|
||||
set_engine_version "${TAG}" "${LAUNCHER_RESOURCES_DIR}"
|
||||
set_mod_version "${TAG}" "${LAUNCHER_RESOURCES_DIR}/mods/${MOD_ID}/mod.yaml" "${LAUNCHER_RESOURCES_DIR}/mods/modcontent/mod.yaml"
|
||||
@@ -108,86 +105,105 @@ build_app() {
|
||||
|
||||
# Sign binaries with developer certificate
|
||||
if [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then
|
||||
codesign -s "${MACOS_DEVELOPER_IDENTITY}" --timestamp --options runtime -f --entitlements entitlements.plist --deep "${LAUNCHER_DIR}"
|
||||
codesign --sign "${MACOS_DEVELOPER_IDENTITY}" --timestamp --options runtime -f --entitlements entitlements.plist --deep "${LAUNCHER_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
build_platform() {
|
||||
PLATFORM="${1}"
|
||||
DMG_PATH="${2}"
|
||||
echo "Building launchers (${PLATFORM})"
|
||||
echo "Building launchers"
|
||||
|
||||
# Prepare generic template for the mods to duplicate and customize
|
||||
TEMPLATE_DIR="${BUILTDIR}/template.app"
|
||||
mkdir -p "${TEMPLATE_DIR}/Contents/Resources"
|
||||
mkdir -p "${TEMPLATE_DIR}/Contents/MacOS"
|
||||
echo "APPL????" > "${TEMPLATE_DIR}/Contents/PkgInfo"
|
||||
cp Info.plist.in "${TEMPLATE_DIR}/Contents/Info.plist"
|
||||
modify_plist "{DEV_VERSION}" "${TAG}" "${TEMPLATE_DIR}/Contents/Info.plist"
|
||||
modify_plist "{FAQ_URL}" "http://wiki.openra.net/FAQ" "${TEMPLATE_DIR}/Contents/Info.plist"
|
||||
# Prepare generic template for the mods to duplicate and customize
|
||||
TEMPLATE_DIR="${BUILTDIR}/template.app"
|
||||
mkdir -p "${TEMPLATE_DIR}/Contents/Resources"
|
||||
mkdir -p "${TEMPLATE_DIR}/Contents/MacOS/mono"
|
||||
mkdir -p "${TEMPLATE_DIR}/Contents/MacOS/x86_64"
|
||||
mkdir -p "${TEMPLATE_DIR}/Contents/MacOS/arm64"
|
||||
|
||||
if [ "${PLATFORM}" = "mono" ]; then
|
||||
modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.9" "${TEMPLATE_DIR}/Contents/Info.plist"
|
||||
clang -m64 launcher-mono.m -o "${TEMPLATE_DIR}/Contents/MacOS/Launcher" -framework AppKit -mmacosx-version-min=10.9
|
||||
else
|
||||
modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.14" "${TEMPLATE_DIR}/Contents/Info.plist"
|
||||
clang -m64 launcher.m -o "${TEMPLATE_DIR}/Contents/MacOS/Launcher" -framework AppKit -mmacosx-version-min=10.14
|
||||
fi
|
||||
echo "APPL????" > "${TEMPLATE_DIR}/Contents/PkgInfo"
|
||||
cp Info.plist.in "${TEMPLATE_DIR}/Contents/Info.plist"
|
||||
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"
|
||||
|
||||
build_app "${PLATFORM}" "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Red Alert.app" "ra" "Red Alert" "699222659766026240"
|
||||
build_app "${PLATFORM}" "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Tiberian Dawn.app" "cnc" "Tiberian Dawn" "699223250181292033"
|
||||
build_app "${PLATFORM}" "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Dune 2000.app" "d2k" "Dune 2000" "712711732770111550"
|
||||
# Compile universal (x86_64 + arm64) Launcher and 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
|
||||
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"
|
||||
|
||||
rm -rf "${TEMPLATE_DIR}"
|
||||
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"
|
||||
|
||||
echo "Packaging disk image"
|
||||
hdiutil create "${DMG_PATH}" -format UDRW -volname "OpenRA" -fs HFS+ -srcfolder build
|
||||
DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_PATH}" | egrep '^/dev/' | sed 1q | awk '{print $1}')
|
||||
sleep 2
|
||||
rm -rf "${TEMPLATE_DIR}"
|
||||
|
||||
# Background image is created from source svg in artsrc repository
|
||||
mkdir "/Volumes/OpenRA/.background/"
|
||||
tiffutil -cathidpicheck "${ARTWORK_DIR}/macos-background.png" "${ARTWORK_DIR}/macos-background-2x.png" -out "/Volumes/OpenRA/.background/background.tiff"
|
||||
echo "Packaging disk image"
|
||||
hdiutil create "build.dmg" -format UDRW -volname "OpenRA" -fs HFS+ -srcfolder build
|
||||
DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "build.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}')
|
||||
sleep 2
|
||||
|
||||
cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns"
|
||||
# Background image is created from source svg in artsrc repository
|
||||
mkdir "/Volumes/OpenRA/.background/"
|
||||
tiffutil -cathidpicheck "${ARTWORK_DIR}/macos-background.png" "${ARTWORK_DIR}/macos-background-2x.png" -out "/Volumes/OpenRA/.background/background.tiff"
|
||||
|
||||
echo '
|
||||
tell application "Finder"
|
||||
tell disk "'OpenRA'"
|
||||
open
|
||||
set current view of container window to icon view
|
||||
set toolbar visible of container window to false
|
||||
set statusbar visible of container window to false
|
||||
set the bounds of container window to {400, 100, 1040, 580}
|
||||
set theViewOptions to the icon view options of container window
|
||||
set arrangement of theViewOptions to not arranged
|
||||
set icon size of theViewOptions to 72
|
||||
set background picture of theViewOptions to file ".background:background.tiff"
|
||||
make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"}
|
||||
set position of item "'OpenRA - Tiberian Dawn.app'" of container window to {160, 106}
|
||||
set position of item "'OpenRA - Red Alert.app'" of container window to {320, 106}
|
||||
set position of item "'OpenRA - Dune 2000.app'" of container window to {480, 106}
|
||||
set position of item "Applications" of container window to {320, 298}
|
||||
set position of item ".background" of container window to {160, 298}
|
||||
set position of item ".fseventsd" of container window to {160, 298}
|
||||
set position of item ".VolumeIcon.icns" of container window to {160, 298}
|
||||
update without registering applications
|
||||
delay 5
|
||||
close
|
||||
end tell
|
||||
end tell
|
||||
' | osascript
|
||||
cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns"
|
||||
|
||||
# HACK: Copy the volume icon again - something in the previous step seems to delete it...?
|
||||
cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns"
|
||||
SetFile -c icnC "/Volumes/OpenRA/.VolumeIcon.icns"
|
||||
SetFile -a C "/Volumes/OpenRA"
|
||||
echo '
|
||||
tell application "Finder"
|
||||
tell disk "'OpenRA'"
|
||||
open
|
||||
set current view of container window to icon view
|
||||
set toolbar visible of container window to false
|
||||
set statusbar visible of container window to false
|
||||
set the bounds of container window to {400, 100, 1040, 580}
|
||||
set theViewOptions to the icon view options of container window
|
||||
set arrangement of theViewOptions to not arranged
|
||||
set icon size of theViewOptions to 72
|
||||
set background picture of theViewOptions to file ".background:background.tiff"
|
||||
make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"}
|
||||
set position of item "'OpenRA - Tiberian Dawn.app'" of container window to {160, 106}
|
||||
set position of item "'OpenRA - Red Alert.app'" of container window to {320, 106}
|
||||
set position of item "'OpenRA - Dune 2000.app'" of container window to {480, 106}
|
||||
set position of item "Applications" of container window to {320, 298}
|
||||
set position of item ".background" of container window to {160, 298}
|
||||
set position of item ".fseventsd" of container window to {160, 298}
|
||||
set position of item ".VolumeIcon.icns" of container window to {160, 298}
|
||||
update without registering applications
|
||||
delay 5
|
||||
close
|
||||
end tell
|
||||
end tell
|
||||
' | osascript
|
||||
|
||||
# Replace duplicate .NET runtime files with hard links to improve compression
|
||||
if [ "${PLATFORM}" != "mono" ]; then
|
||||
for MOD in "Red Alert" "Tiberian Dawn"; do
|
||||
for f in "/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS"/*; do
|
||||
g="/Volumes/OpenRA/OpenRA - Dune 2000.app/Contents/MacOS/"$(basename "${f}")
|
||||
# HACK: Copy the volume icon again - something in the previous step seems to delete it...?
|
||||
cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns"
|
||||
SetFile -c icnC "/Volumes/OpenRA/.VolumeIcon.icns"
|
||||
SetFile -a C "/Volumes/OpenRA"
|
||||
|
||||
# Replace duplicate .NET runtime files with hard links to improve compression
|
||||
for MOD in "Red Alert" "Tiberian Dawn"; do
|
||||
for p in "x86_64" "arm64" "mono"; do
|
||||
for f in "/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS/${p}"/*; do
|
||||
g="/Volumes/OpenRA/OpenRA - Dune 2000.app/Contents/MacOS/${p}/"$(basename "${f}")
|
||||
hashf=$(shasum "${f}" | awk '{ print $1 }') || :
|
||||
hashg=$(shasum "${g}" | awk '{ print $1 }') || :
|
||||
if [ -n "${hashf}" ] && [ "${hashf}" = "${hashg}" ]; then
|
||||
echo "Deduplicating ${f}"
|
||||
rm "${f}"
|
||||
ln "${g}" "${f}"
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
for MOD in "Red Alert" "Tiberian Dawn" "Dune 2000"; do
|
||||
for p in "arm64" "mono"; do
|
||||
for f in "/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS/x86_64"/*; do
|
||||
g="/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS/${p}/"$(basename "${f}")
|
||||
if [ -e "${g}" ]; then
|
||||
hashf=$(shasum "${f}" | awk '{ print $1 }') || :
|
||||
hashg=$(shasum "${g}" | awk '{ print $1 }') || :
|
||||
if [ -n "${hashf}" ] && [ "${hashf}" = "${hashg}" ]; then
|
||||
@@ -195,101 +211,48 @@ build_platform() {
|
||||
rm "${f}"
|
||||
ln "${g}" "${f}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
chmod -Rf go-w /Volumes/OpenRA
|
||||
sync
|
||||
sync
|
||||
|
||||
hdiutil detach "${DMG_DEVICE}"
|
||||
rm -rf "${BUILTDIR}"
|
||||
}
|
||||
|
||||
notarize_package() {
|
||||
DMG_PATH="${1}"
|
||||
NOTARIZE_DMG_PATH="${DMG_PATH%.*}"-notarization.dmg
|
||||
echo "Submitting ${DMG_PATH} for notarization"
|
||||
|
||||
# Reset xcode search path to fix xcrun not finding altool
|
||||
sudo xcode-select -r
|
||||
|
||||
# Create a temporary read-only dmg for submission (notarization service rejects read/write images)
|
||||
hdiutil convert "${DMG_PATH}" -format ULFO -ov -o "${NOTARIZE_DMG_PATH}"
|
||||
|
||||
NOTARIZATION_UUID=$(xcrun altool --notarize-app --primary-bundle-id "net.openra.packaging" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" --file "${NOTARIZE_DMG_PATH}" 2>&1 | awk -F' = ' '/RequestUUID/ { print $2; exit }')
|
||||
if [ -z "${NOTARIZATION_UUID}" ]; then
|
||||
echo "Submission failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${DMG_PATH} submission UUID is ${NOTARIZATION_UUID}"
|
||||
rm "${NOTARIZE_DMG_PATH}"
|
||||
|
||||
while :; do
|
||||
sleep 30
|
||||
NOTARIZATION_RESULT=$(xcrun altool --notarization-info "${NOTARIZATION_UUID}" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" 2>&1 | awk -F': ' '/Status/ { print $2; exit }')
|
||||
echo "${DMG_PATH}: ${NOTARIZATION_RESULT}"
|
||||
|
||||
if [ "${NOTARIZATION_RESULT}" == "invalid" ]; then
|
||||
NOTARIZATION_LOG_URL=$(xcrun altool --notarization-info "${NOTARIZATION_UUID}" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" 2>&1 | awk -F': ' '/LogFileURL/ { print $2; exit }')
|
||||
echo "${NOTARIZATION_UUID} failed notarization with error:"
|
||||
curl -s "${NOTARIZATION_LOG_URL}" -w "\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${NOTARIZATION_RESULT}" == "success" ]; then
|
||||
echo "${DMG_PATH}: Stapling tickets"
|
||||
DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_PATH}" | egrep '^/dev/' | sed 1q | awk '{print $1}')
|
||||
sleep 2
|
||||
|
||||
xcrun stapler staple "/Volumes/OpenRA/OpenRA - Red Alert.app"
|
||||
xcrun stapler staple "/Volumes/OpenRA/OpenRA - Tiberian Dawn.app"
|
||||
xcrun stapler staple "/Volumes/OpenRA/OpenRA - Dune 2000.app"
|
||||
|
||||
sync
|
||||
sync
|
||||
|
||||
hdiutil detach "${DMG_DEVICE}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
done
|
||||
|
||||
finalize_package() {
|
||||
PLATFORM="${1}"
|
||||
INPUT_PATH="${2}"
|
||||
OUTPUT_PATH="${3}"
|
||||
chmod -Rf go-w /Volumes/OpenRA
|
||||
sync
|
||||
sync
|
||||
|
||||
if [ "${PLATFORM}" = "mono" ]; then
|
||||
hdiutil convert "${INPUT_PATH}" -format UDZO -imagekey zlib-level=9 -ov -o "${OUTPUT_PATH}"
|
||||
else
|
||||
# ULFO offers better compression and faster decompression speeds, but is only supported by 10.11+
|
||||
hdiutil convert "${INPUT_PATH}" -format ULFO -ov -o "${OUTPUT_PATH}"
|
||||
fi
|
||||
|
||||
rm "${INPUT_PATH}"
|
||||
}
|
||||
|
||||
build_platform "standard" "build.dmg"
|
||||
build_platform "mono" "build-mono.dmg"
|
||||
hdiutil detach "${DMG_DEVICE}"
|
||||
rm -rf "${BUILTDIR}"
|
||||
|
||||
if [ -n "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" ] && [ -n "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then
|
||||
security delete-keychain build.keychain
|
||||
fi
|
||||
|
||||
if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ]; then
|
||||
# Parallelize processing
|
||||
(notarize_package "build.dmg") || exit 1 &
|
||||
(notarize_package "build-mono.dmg") || exit 1 &
|
||||
while wait -n; rc=$?; [ "${rc}" != 127 ]; do
|
||||
if [ "${rc}" != 0 ]; then
|
||||
wait
|
||||
exit "${rc}"
|
||||
fi
|
||||
done
|
||||
if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then
|
||||
echo "Submitting build for notarization"
|
||||
|
||||
# Reset xcode search path to fix xcrun not finding altool
|
||||
sudo xcode-select -r
|
||||
|
||||
# Create a temporary read-only dmg for submission (notarization service rejects read/write images)
|
||||
hdiutil convert "build.dmg" -format ULFO -ov -o "build-notarization.dmg"
|
||||
|
||||
xcrun notarytool submit "build-notarization.dmg" --wait --apple-id "${MACOS_DEVELOPER_USERNAME}" --password "${MACOS_DEVELOPER_PASSWORD}" --team-id "${MACOS_DEVELOPER_IDENTITY}"
|
||||
|
||||
rm "build-notarization.dmg"
|
||||
|
||||
echo "Stapling tickets"
|
||||
DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "build.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}')
|
||||
sleep 2
|
||||
|
||||
xcrun stapler staple "/Volumes/OpenRA/OpenRA - Red Alert.app"
|
||||
xcrun stapler staple "/Volumes/OpenRA/OpenRA - Tiberian Dawn.app"
|
||||
xcrun stapler staple "/Volumes/OpenRA/OpenRA - Dune 2000.app"
|
||||
|
||||
sync
|
||||
sync
|
||||
|
||||
hdiutil detach "${DMG_DEVICE}"
|
||||
fi
|
||||
|
||||
finalize_package "standard" "build.dmg" "${OUTPUTDIR}/OpenRA-${TAG}.dmg"
|
||||
finalize_package "mono" "build-mono.dmg" "${OUTPUTDIR}/OpenRA-${TAG}-mono.dmg"
|
||||
hdiutil convert "build.dmg" -format ULFO -ov -o "${OUTPUTDIR}/OpenRA-${TAG}.dmg"
|
||||
rm "build.dmg"
|
||||
|
||||
73
packaging/macos/checkmono.c
Normal file
73
packaging/macos/checkmono.c
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
|
||||
//
|
||||
// NOTE: Mono.framework only ships intel dylibs, so cannot be loaded by the arm64 slice of the Launcher utility.
|
||||
// Splitting checkmono into its own intel-only utility allows it to be called through rosetta if the user
|
||||
// wants to force the game to run under mono-through-rosetta.
|
||||
//
|
||||
// Based on https://github.com/mono/monodevelop/blob/main/main/build/MacOSX/monostub.mm and https://github.com/mono/monodevelop/blob/main/main/build/MacOSX/monostub-utils.h
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#define SYSTEM_MONO_PATH "/Library/Frameworks/Mono.framework/Versions/Current/"
|
||||
#define SYSTEM_MONO_MIN_VERSION "6.4"
|
||||
|
||||
typedef char *(* mono_get_runtime_build_info)(void);
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
void *libmono = dlopen(SYSTEM_MONO_PATH "lib/libmonosgen-2.0.dylib", RTLD_LAZY);
|
||||
if (libmono == NULL)
|
||||
{
|
||||
fprintf (stderr, "Failed to load libmonosgen-2.0.dylib: %s\n", dlerror());
|
||||
return 1;
|
||||
}
|
||||
|
||||
mono_get_runtime_build_info _mono_get_runtime_build_info = (mono_get_runtime_build_info)dlsym(libmono, "mono_get_runtime_build_info");
|
||||
if (!_mono_get_runtime_build_info)
|
||||
{
|
||||
fprintf(stderr, "Could not load mono_get_runtime_build_info(): %s\n", dlerror());
|
||||
return 1;
|
||||
}
|
||||
|
||||
char *version = _mono_get_runtime_build_info();
|
||||
char *req_end, *end;
|
||||
long req_val, val;
|
||||
char *req_version = SYSTEM_MONO_MIN_VERSION;
|
||||
|
||||
while (*req_version && *version)
|
||||
{
|
||||
req_val = strtol(req_version, &req_end, 10);
|
||||
if (req_version == req_end || (*req_end && *req_end != '.'))
|
||||
{
|
||||
fprintf(stderr, "Bad version requirement string '%s'\n", req_end);
|
||||
return 1;
|
||||
}
|
||||
|
||||
val = strtol(version, &end, 10);
|
||||
if (version == end || val < req_val)
|
||||
return 1;
|
||||
|
||||
if (val > req_val)
|
||||
return 0;
|
||||
|
||||
if (*req_end == '.' && *end != '.')
|
||||
return 1;
|
||||
|
||||
req_version = req_end;
|
||||
if (*req_version)
|
||||
req_version++;
|
||||
|
||||
version = end + 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
/*
|
||||
* 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#define SYSTEM_MONO_PATH @"/Library/Frameworks/Mono.framework/Versions/Current/"
|
||||
#define SYSTEM_MONO_MIN_VERSION @"6.4"
|
||||
|
||||
typedef int (* mono_main)(int argc, char **argv);
|
||||
typedef void (* mono_free)(void *ptr);
|
||||
typedef char *(* mono_get_runtime_build_info)(void);
|
||||
|
||||
@interface OpenRALauncher : NSObject <NSApplicationDelegate>
|
||||
- (void)launchGameWithArgs: (NSArray *)gameArgs;
|
||||
@end
|
||||
|
||||
@implementation OpenRALauncher
|
||||
|
||||
BOOL launched = NO;
|
||||
NSTask *gameTask;
|
||||
|
||||
static int check_mono_version(const char *version, const char *req_version)
|
||||
{
|
||||
char *req_end, *end;
|
||||
long req_val, val;
|
||||
|
||||
while (*req_version)
|
||||
{
|
||||
req_val = strtol(req_version, &req_end, 10);
|
||||
if (req_version == req_end || (*req_end && *req_end != '.'))
|
||||
{
|
||||
fprintf(stderr, "Bad version requirement string '%s'\n", req_end);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
req_version = req_end;
|
||||
if (*req_version)
|
||||
req_version++;
|
||||
|
||||
val = strtol (version, &end, 10);
|
||||
if (version == end || val < req_val)
|
||||
return FALSE;
|
||||
|
||||
if (val > req_val)
|
||||
return TRUE;
|
||||
|
||||
if (*req_version == '.' && *end != '.')
|
||||
return FALSE;
|
||||
|
||||
version = end + 1;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
- (int)hasValidMono
|
||||
{
|
||||
void *libmono = dlopen([[SYSTEM_MONO_PATH stringByAppendingPathComponent: @"/lib/libmonosgen-2.0.dylib"] UTF8String], RTLD_LAZY);
|
||||
|
||||
if (libmono == NULL)
|
||||
{
|
||||
fprintf (stderr, "Failed to load libmonosgen-2.0.dylib: %s\n", dlerror());
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
mono_get_runtime_build_info _mono_get_runtime_build_info = (mono_get_runtime_build_info)dlsym(libmono, "mono_get_runtime_build_info");
|
||||
if (!_mono_get_runtime_build_info)
|
||||
{
|
||||
fprintf(stderr, "Could not load mono_get_runtime_build_info(): %s\n", dlerror());
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
char *mono_version = _mono_get_runtime_build_info();
|
||||
return check_mono_version(mono_version, [SYSTEM_MONO_MIN_VERSION UTF8String]);
|
||||
}
|
||||
|
||||
- (NSString *)modName
|
||||
{
|
||||
NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
|
||||
if (plist)
|
||||
{
|
||||
NSString *title = [plist objectForKey:@"CFBundleDisplayName"];
|
||||
if (title && [title length] > 0)
|
||||
return title;
|
||||
}
|
||||
|
||||
return @"OpenRA";
|
||||
}
|
||||
|
||||
- (void)exitWithMonoPrompt
|
||||
{
|
||||
[NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
|
||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||
|
||||
NSString *modName = [self modName];
|
||||
NSString *title = [NSString stringWithFormat: @"Cannot launch %@", modName];
|
||||
NSString *message = [NSString stringWithFormat: @"%@ requires Mono %@ or later. Please install Mono and try again.", modName, SYSTEM_MONO_MIN_VERSION];
|
||||
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setMessageText:title];
|
||||
[alert setInformativeText:message];
|
||||
[alert addButtonWithTitle:@"Download Mono"];
|
||||
[alert addButtonWithTitle:@"Quit"];
|
||||
NSInteger answer = [alert runModal];
|
||||
[alert release];
|
||||
|
||||
if (answer == NSAlertFirstButtonReturn)
|
||||
[[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"https://www.mono-project.com/download/"]];
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
- (void)exitWithCrashPrompt
|
||||
{
|
||||
[NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
|
||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||
|
||||
NSString *modName = [self modName];
|
||||
NSString *message = [NSString stringWithFormat: @"%@ has encountered a fatal error and must close.\nPlease refer to the crash logs and FAQ for more information.", modName];
|
||||
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setMessageText:@"Fatal Error"];
|
||||
[alert setInformativeText:message];
|
||||
[alert addButtonWithTitle:@"View Logs"];
|
||||
[alert addButtonWithTitle:@"View FAQ"];
|
||||
[alert addButtonWithTitle:@"Quit"];
|
||||
|
||||
NSInteger answer = [alert runModal];
|
||||
[alert release];
|
||||
|
||||
if (answer == NSAlertFirstButtonReturn)
|
||||
{
|
||||
NSString *logDir = [@"~/Library/Application Support/OpenRA/Logs/" stringByExpandingTildeInPath];
|
||||
[[NSWorkspace sharedWorkspace] openFile: logDir withApplication:@"Finder"];
|
||||
}
|
||||
else if (answer == NSAlertSecondButtonReturn)
|
||||
{
|
||||
NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
|
||||
if (plist)
|
||||
{
|
||||
NSString *faqUrl = [plist objectForKey:@"FaqUrl"];
|
||||
if (faqUrl && [faqUrl length] > 0)
|
||||
[[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:faqUrl]];
|
||||
}
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Application was launched via a URL handler
|
||||
- (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
|
||||
{
|
||||
NSMutableArray *gameArgs = [[[NSProcessInfo processInfo] arguments] mutableCopy];
|
||||
NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
|
||||
NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
|
||||
|
||||
if (plist)
|
||||
{
|
||||
NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"];
|
||||
if (joinServerUrl && [joinServerUrl length] > 0)
|
||||
{
|
||||
NSString *prefix = [joinServerUrl stringByAppendingString: @"://"];
|
||||
if ([url hasPrefix: prefix])
|
||||
{
|
||||
NSString *trimmed = [url substringFromIndex:[prefix length]];
|
||||
NSArray *parts = [trimmed componentsSeparatedByString:@":"];
|
||||
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
|
||||
|
||||
if ([parts count] == 2 && [formatter numberFromString: [parts objectAtIndex:1]] != nil)
|
||||
[gameArgs addObject: [NSString stringWithFormat: @"Launch.Connect=%@", trimmed]];
|
||||
|
||||
[formatter release];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[self launchGameWithArgs: gameArgs];
|
||||
[gameArgs release];
|
||||
}
|
||||
|
||||
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
|
||||
{
|
||||
// Register for url events
|
||||
NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
|
||||
if (plist)
|
||||
{
|
||||
NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"];
|
||||
NSString *bundleIdentifier = [plist objectForKey:@"CFBundleIdentifier"];
|
||||
if (joinServerUrl && [joinServerUrl length] > 0 && bundleIdentifier)
|
||||
{
|
||||
LSSetDefaultHandlerForURLScheme((CFStringRef)joinServerUrl, (CFStringRef)bundleIdentifier);
|
||||
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(getUrl:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
||||
{
|
||||
[self launchGameWithArgs: [[NSProcessInfo processInfo] arguments]];
|
||||
}
|
||||
|
||||
- (BOOL)applicationShouldTerminateAfterLastWindowClosed: (NSApplication *)theApplication
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)launchGameWithArgs: (NSArray *)gameArgs
|
||||
{
|
||||
if (launched)
|
||||
{
|
||||
NSLog(@"launchgame is already running... ignoring request.");
|
||||
return;
|
||||
}
|
||||
|
||||
launched = YES;
|
||||
|
||||
if (![self hasValidMono])
|
||||
[self exitWithMonoPrompt];
|
||||
|
||||
// Default values - can be overriden by setting certain keys Info.plist
|
||||
NSString *gameName = @"OpenRA.dll";
|
||||
NSString *modId = nil;
|
||||
|
||||
NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
|
||||
if (plist)
|
||||
{
|
||||
NSString *exeValue = [plist objectForKey:@"MonoGameExe"];
|
||||
if (exeValue && [exeValue length] > 0)
|
||||
gameName = exeValue;
|
||||
|
||||
NSString *modIdValue = [plist objectForKey:@"ModId"];
|
||||
if (modIdValue && [modIdValue length] > 0)
|
||||
modId = modIdValue;
|
||||
}
|
||||
|
||||
NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"];
|
||||
NSString *gamePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/Resources/"];
|
||||
NSString *appPath = [exePath stringByAppendingPathComponent: @"Launcher"];
|
||||
NSString *engineLaunchPath = [self resolveTranslocatedPath: appPath];
|
||||
|
||||
NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 2];
|
||||
[launchArgs addObject: @"--debug"];
|
||||
[launchArgs addObject: [exePath stringByAppendingPathComponent: gameName]];
|
||||
[launchArgs addObject: [NSString stringWithFormat:@"Engine.LaunchPath=\"%@\"", engineLaunchPath]];
|
||||
[launchArgs addObject: [NSString stringWithFormat:@"Engine.EngineDir=../Resources"]];
|
||||
|
||||
if (modId)
|
||||
[launchArgs addObject: [NSString stringWithFormat:@"Game.Mod=%@", modId]];
|
||||
|
||||
[launchArgs addObjectsFromArray: gameArgs];
|
||||
|
||||
NSLog(@"Running launchgame with arguments:");
|
||||
for (size_t i = 0; i < [launchArgs count]; i++)
|
||||
NSLog(@"%@", [launchArgs objectAtIndex: i]);
|
||||
|
||||
gameTask = [[NSTask alloc] init];
|
||||
[gameTask setCurrentDirectoryPath: gamePath];
|
||||
[gameTask setLaunchPath: appPath];
|
||||
[gameTask setArguments: launchArgs];
|
||||
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver: self
|
||||
selector: @selector(taskExited:)
|
||||
name: NSTaskDidTerminateNotification
|
||||
object: gameTask
|
||||
];
|
||||
|
||||
[gameTask launch];
|
||||
}
|
||||
|
||||
- (NSString *)resolveTranslocatedPath: (NSString *)path
|
||||
{
|
||||
// macOS 10.12 introduced the "App Translocation" feature, which runs quarantined applications
|
||||
// from a transient read-only disk image. The read-only image isn't a problem, but the transient
|
||||
// path breaks the mod registration/switching feature.
|
||||
// This resolves the original path which can then be written into the mod metadata for future
|
||||
// launches (which will then be re-translocated)
|
||||
|
||||
// Running on macOS < 10.12
|
||||
if (floor(NSAppKitVersionNumber) <= 1404)
|
||||
return path;
|
||||
|
||||
void *handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY);
|
||||
|
||||
// Failed to load security framework
|
||||
if (handle == NULL)
|
||||
return path;
|
||||
|
||||
Boolean (*mySecTranslocateIsTranslocatedURL)(CFURLRef path, bool *isTranslocated, CFErrorRef * __nullable error);
|
||||
mySecTranslocateIsTranslocatedURL = dlsym(handle, "SecTranslocateIsTranslocatedURL");
|
||||
|
||||
CFURLRef __nullable (*mySecTranslocateCreateOriginalPathForURL)(CFURLRef translocatedPath, CFErrorRef * __nullable error);
|
||||
mySecTranslocateCreateOriginalPathForURL = dlsym(handle, "SecTranslocateCreateOriginalPathForURL");
|
||||
|
||||
// Failed to resolve required functions
|
||||
if (mySecTranslocateIsTranslocatedURL == NULL || mySecTranslocateCreateOriginalPathForURL == NULL)
|
||||
return path;
|
||||
|
||||
bool isTranslocated = false;
|
||||
CFURLRef pathURLRef = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)path, kCFURLPOSIXPathStyle, false);
|
||||
|
||||
if (mySecTranslocateIsTranslocatedURL(pathURLRef, &isTranslocated, NULL))
|
||||
{
|
||||
if (isTranslocated)
|
||||
{
|
||||
CFURLRef resolvedURL = mySecTranslocateCreateOriginalPathForURL(pathURLRef, NULL);
|
||||
path = [(NSURL *)(resolvedURL) path];
|
||||
}
|
||||
}
|
||||
|
||||
CFRelease(pathURLRef);
|
||||
return path;
|
||||
}
|
||||
|
||||
- (void)taskExited:(NSNotification *)note
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
removeObserver:self
|
||||
name:NSTaskDidTerminateNotification
|
||||
object:gameTask
|
||||
];
|
||||
|
||||
int ret = [gameTask terminationStatus];
|
||||
NSLog(@"launchgame exited with code %d", ret);
|
||||
[gameTask release];
|
||||
gameTask = nil;
|
||||
|
||||
// We're done here
|
||||
if (ret != 0)
|
||||
[self exitWithCrashPrompt];
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
|
||||
if (argc > 1)
|
||||
{
|
||||
struct rlimit limit;
|
||||
if (getrlimit (RLIMIT_NOFILE, &limit) == 0 && limit.rlim_cur < 1024)
|
||||
{
|
||||
limit.rlim_cur = MIN(limit.rlim_max, 1024);
|
||||
setrlimit(RLIMIT_NOFILE, &limit);
|
||||
}
|
||||
|
||||
void *libmono = dlopen([[SYSTEM_MONO_PATH stringByAppendingPathComponent: @"/lib/libmonosgen-2.0.dylib"] UTF8String], RTLD_LAZY);
|
||||
if (libmono == NULL)
|
||||
{
|
||||
fprintf (stderr, "Failed to load libmonosgen-2.0.dylib: %s\n", dlerror());
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
mono_main _mono_main = (mono_main)dlsym(libmono, "mono_main");
|
||||
if (!_mono_main)
|
||||
{
|
||||
fprintf(stderr, "Could not load mono_main(): %s\n", dlerror());
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
[pool drain];
|
||||
|
||||
return _mono_main(argc, argv);
|
||||
}
|
||||
|
||||
NSApplication *application = [NSApplication sharedApplication];
|
||||
OpenRALauncher *launcher = [[OpenRALauncher alloc] init];
|
||||
[NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited];
|
||||
|
||||
[application setDelegate:launcher];
|
||||
[application run];
|
||||
|
||||
[launcher release];
|
||||
[pool drain];
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <dlfcn.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/sysctl.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
|
||||
|
||||
@interface OpenRALauncher : NSObject <NSApplicationDelegate>
|
||||
- (void)launchGameWithArgs: (NSArray *)gameArgs;
|
||||
@@ -31,8 +38,34 @@ NSTask *gameTask;
|
||||
return @"OpenRA";
|
||||
}
|
||||
|
||||
- (void)showCrashPrompt
|
||||
- (void)exitWithMonoPrompt
|
||||
{
|
||||
[NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
|
||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||
|
||||
NSString *modName = [self modName];
|
||||
NSString *title = [NSString stringWithFormat: @"Cannot launch %@", modName];
|
||||
NSString *message = [NSString stringWithFormat: @"%@ requires Mono %@ or later. Please install Mono and try again.", modName, SYSTEM_MONO_MIN_VERSION];
|
||||
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setMessageText:title];
|
||||
[alert setInformativeText:message];
|
||||
[alert addButtonWithTitle:@"Download Mono"];
|
||||
[alert addButtonWithTitle:@"Quit"];
|
||||
NSInteger answer = [alert runModal];
|
||||
[alert release];
|
||||
|
||||
if (answer == NSAlertFirstButtonReturn)
|
||||
[[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"https://www.mono-project.com/download/"]];
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
- (void)exitWithCrashPrompt
|
||||
{
|
||||
[NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
|
||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||
|
||||
NSString *modName = [self modName];
|
||||
NSString *message = [NSString stringWithFormat: @"%@ has encountered a fatal error and must close.\nPlease refer to the crash logs and FAQ for more information.", modName];
|
||||
|
||||
@@ -61,6 +94,8 @@ NSTask *gameTask;
|
||||
[[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:faqUrl]];
|
||||
}
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Application was launched via a URL handler
|
||||
@@ -120,6 +155,16 @@ NSTask *gameTask;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (int)hasValidMono
|
||||
{
|
||||
NSTask *task = [[NSTask alloc] init];
|
||||
[task setLaunchPath: [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/checkmono"]];
|
||||
[task launch];
|
||||
[task waitUntilExit];
|
||||
|
||||
return [task terminationStatus] == 0;
|
||||
}
|
||||
|
||||
- (void)launchGameWithArgs: (NSArray *)gameArgs
|
||||
{
|
||||
if (launched)
|
||||
@@ -130,6 +175,16 @@ NSTask *gameTask;
|
||||
|
||||
launched = YES;
|
||||
|
||||
BOOL useMono = NO;
|
||||
|
||||
if (@available(macOS 10.15, *))
|
||||
useMono = [[[NSProcessInfo processInfo] environment]objectForKey:@"OPENRA_PREFER_MONO"] != nil;
|
||||
else
|
||||
useMono = YES;
|
||||
|
||||
if (useMono && ![self hasValidMono])
|
||||
[self exitWithMonoPrompt];
|
||||
|
||||
// Default values - can be overriden by setting certain keys Info.plist
|
||||
NSString *modId = nil;
|
||||
|
||||
@@ -144,13 +199,44 @@ NSTask *gameTask;
|
||||
NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"];
|
||||
NSString *gamePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/Resources/"];
|
||||
|
||||
NSString *launchPath = [exePath stringByAppendingPathComponent: @"OpenRA"];
|
||||
NSString *launchPath;
|
||||
NSString *dllPath;
|
||||
NSString *hostPath;
|
||||
|
||||
if (useMono)
|
||||
{
|
||||
launchPath = [exePath stringByAppendingPathComponent: @"apphost-mono"];
|
||||
hostPath = [SYSTEM_MONO_PATH stringByAppendingPathComponent: @"lib/libmonosgen-2.0.dylib"];;
|
||||
dllPath = [exePath stringByAppendingPathComponent: @"mono/OpenRA.dll"];
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t size;
|
||||
cpu_type_t type;
|
||||
size = sizeof(type);
|
||||
|
||||
if (sysctlbyname("hw.cputype", &type, &size, NULL, 0) == 0 && (type & 0xFF) == CPU_TYPE_ARM)
|
||||
{
|
||||
launchPath = [exePath stringByAppendingPathComponent: @"apphost-arm64"];
|
||||
hostPath = [exePath stringByAppendingPathComponent: @"arm64/libhostfxr.dylib"];;
|
||||
dllPath = [exePath stringByAppendingPathComponent: @"arm64/OpenRA.dll"];
|
||||
}
|
||||
else
|
||||
{
|
||||
launchPath = [exePath stringByAppendingPathComponent: @"apphost-x86_64"];
|
||||
hostPath = [exePath stringByAppendingPathComponent: @"x86_64/libhostfxr.dylib"];;
|
||||
dllPath = [exePath stringByAppendingPathComponent: @"x86_64/OpenRA.dll"];
|
||||
}
|
||||
}
|
||||
|
||||
NSString *appPath = [exePath stringByAppendingPathComponent: @"Launcher"];
|
||||
NSString *engineLaunchPath = [self resolveTranslocatedPath: appPath];
|
||||
|
||||
NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 2];
|
||||
NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 5];
|
||||
[launchArgs addObject: hostPath];
|
||||
[launchArgs addObject: dllPath];
|
||||
[launchArgs addObject: [NSString stringWithFormat:@"Engine.LaunchPath=\"%@\"", engineLaunchPath]];
|
||||
[launchArgs addObject: [NSString stringWithFormat:@"Engine.EngineDir=../Resources"]];
|
||||
[launchArgs addObject: [NSString stringWithFormat:@"Engine.EngineDir=../../Resources"]];
|
||||
|
||||
if (modId)
|
||||
[launchArgs addObject: [NSString stringWithFormat:@"Game.Mod=%@", modId]];
|
||||
@@ -229,21 +315,15 @@ NSTask *gameTask;
|
||||
];
|
||||
|
||||
int ret = [gameTask terminationStatus];
|
||||
|
||||
NSLog(@"launchgame exited with code %d", ret);
|
||||
[gameTask release];
|
||||
gameTask = nil;
|
||||
|
||||
// We're done here
|
||||
if (ret == 0)
|
||||
exit(0);
|
||||
if (ret != 0)
|
||||
[self exitWithCrashPrompt];
|
||||
|
||||
// Make the error dialog visible
|
||||
[NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
|
||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||
[self showCrashPrompt];
|
||||
|
||||
exit(1);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user