Compare commits

...

49 Commits

Author SHA1 Message Date
Paul Chote
7691507baf Fix pseudo-fullscreen window size on macs with a notch. 2022-12-24 12:12:12 +01:00
abcdefg30
8d6cebe654 Fix map actors not being spawned with the correct owner 2022-12-24 11:20:58 +02:00
Matthias Mailänder
daf10c34a6 Fix invalid actors not spawning in-game. 2022-12-23 18:33:50 +01:00
Matthias Mailänder
d96ec21b95 In map editor replace invalid actor owners with neutral. 2022-12-23 18:33:49 +01:00
Paul Chote
aea1182bb5 Implement state prediction for lobby ready checkbox. 2022-12-23 16:34:05 +02:00
Paul Chote
b413b97a52 Implement state prediction for debug menu checkboxes. 2022-12-23 16:34:05 +02:00
Paul Chote
7daa27f123 Implement state prediction for lobby checkboxes. 2022-12-23 16:34:05 +02:00
Paul Chote
f3f98d8750 Package macOS releases as a universal binary.
* Minimum macOS version is raised to 10.11.
* App bundles ship 3 versions of the runtime and engine binaries,
  and a fat launcher that selects the appropriate runtime/apphost.
* Mono is used for macOS 10.11 - 10.14, or if OPENRA_PREFER_MONO
  environment variable has been set.
2022-12-23 12:55:53 +02:00
Paul Chote
69eeb2dc84 Update native deps to include macOS and Linux arm64 binaries. 2022-12-23 12:55:53 +02:00
Paul Chote
7cee29ff70 Report CPU arch in logs and sysinfo. 2022-12-23 12:54:29 +02:00
Paul Chote
5e80fee913 Fix MiniYaml source locations being lost when merging. 2022-12-23 10:45:24 +02:00
Gustas
c1474204e2 Added carryalls to spectator Economy statistics 2022-12-21 21:09:16 +01:00
N.N
223f9408fd Change tile 9 to Rough TerrainType 2022-12-21 19:57:19 +02:00
Gustas
674ca8555b Fix contrail using the wrong colors 2022-12-21 19:36:12 +02:00
abcdefg30
275325b0cf Run the CI workflow on PRs targeting prep 2022-12-21 13:28:48 +02:00
Paul Chote
06e399d3ce Bubble unhandled double-click events to OnClick. 2022-12-20 23:06:19 +01:00
RoosterDragon
d6d77eab7c HPF is aware of map projection changes.
An event is added to Map to indicate when the cell projection is changed. This is important as this can mean Map.Contains(CPos) could now return different results for the cell. The HierarchicalPathFinder is made aware of these changes so it can rebuild any out-of-date information. This fixes prevent a crash if a cell that was previously outside the map changes height and becomes inside the map. The local path search will explore the cell as it is inside the map - but if the HPF was unaware if had been updated, it will still consider the cell to be outside the map and unreachable, resulting in a crash.
2022-12-20 09:48:44 +13:00
RoosterDragon
2392566c1d HPF handles searches from unreachable source cells into cut off areas.
If a path search is performed by the HierarchicalPathFinder when the source cell is unreachable location, a path is still allowed and starts from one of the cells adjacent to the location. If moving into one of these cells results in the actor moving into an isolated area that cannot reach the target this would previously crash as no abstract path could be found. Now we handle such locations by giving them a unreachable cost so the path search will not attempt to explore them.

Imagine a map split into two by a one tile wide line of impassable cliffs. If an actor is on top of these cliffs it is allowed to path because it can "jump off" the cliff and move into the cell immediately either side of it. However it is important which side it chooses to jump into, as one it has moved off the cliff it loses access to the other side. The previous error came about because the path might try and search on the side that couldn't reach the target location. Now we avoid that being considered.
2022-12-20 09:48:36 +13:00
Matthias Mailänder
028467f150 Fix documentation deployment. 2022-12-18 12:57:21 +13:00
abcdefg30
eee4b04248 Fix the actor edit panel not always getting closed properly 2022-12-18 12:25:21 +13:00
Gustas
f551b542f3 Fix map editor sliders stealing focus 2022-12-15 21:06:25 +01:00
Smittytron
5d5419702b Remove MustDestroy from longbows in Soviet 11 2022-12-10 16:16:24 +02:00
Smittytron
47e89b7d76 Fix Siberian Conflict 3 win trigger 2022-12-10 16:16:13 +02:00
N.N
9bb0409f19 Update D2k map pool 2022-12-07 23:39:44 +01:00
abcdefg30
80ac494948 Fix carryables vanishing when another unit is picked up 2022-12-04 17:41:44 +02:00
abcdefg30
eb82f2b975 Remove a stray $ from infront of the version string when writing logs 2022-12-03 23:49:24 +02:00
abcdefg30
a5c8db82da Mark the script as executable 2022-12-02 23:32:41 +01:00
abcdefg30
2a26ff6818 Revert "Update Ubuntu"
This reverts commit ac351f6e65.
2022-12-02 23:32:39 +01:00
Matthias Mailänder
5a11d54956 Execute self-contained binaries with runtime as backfall 2022-12-02 23:03:58 +01:00
Gustas
26c5a8b76c Add cell reference HPF crash messages 2022-12-02 17:50:13 +01:00
Matthias Mailänder
efcfab78dc Unify file extensions. 2022-12-01 19:54:07 +02:00
Matthias Mailänder
ac351f6e65 Update Ubuntu 2022-12-01 19:54:07 +02:00
abcdefg30
42bdc9e53e Replace influence instead of throwing an exception in AddInfluence 2022-12-01 18:13:16 +01:00
Gustas
3097efc5b6 Fix UnreserveCarryable unreserving carryables of other carriers 2022-12-01 18:13:16 +01:00
Gustas
853422409f Fix AutoCaryall not unreserving when cargo dies 2022-12-01 18:13:16 +01:00
abcdefg30
f097250394 Make sure PickupUnit runs a TakeOff activity before ending 2022-12-01 18:13:16 +01:00
Paul Chote
96e0f96c12 Remove redundant order copies. 2022-11-27 23:00:53 +01:00
Paul Chote
0a4c4162be Always serialize orders. 2022-11-27 23:00:52 +01:00
Paul Chote
10ff24f24e Fix frame number not being included in pre-serialized order packets. 2022-11-27 23:00:51 +01:00
Matthias Mailänder
aeb96d98f3 Update notarisation to XCode 13 tooling. 2022-11-27 20:32:57 +01:00
Paul Chote
cf4bfdcdfb Fix display bounds when running on macbooks with a notch. 2022-11-26 20:45:22 +01:00
dnqbob
d55af8d75d HarvesterBotModule should not command harvesters that cannot be ordered 2022-11-24 23:22:04 +01:00
Matthias Mailänder
dd9e75d017 Show the host in the download failed error message. 2022-11-23 23:36:10 +01:00
Matthias Mailänder
1b6220962e Fix unknown host not getting translated. 2022-11-23 23:36:08 +01:00
Vapre
f70f2acb39 ExceptionHandler, fix npe. 2022-11-22 12:35:33 +01:00
Matthias Mailänder
32e2507bff Revert "Scripts: Check exit status of background process"
This reverts commit 3f106bef72.
2022-11-20 20:36:58 +02:00
Matthias Mailänder
22a42b7dc9 Manually add game speeds to the linter. 2022-11-18 23:14:54 +01:00
Gustas
c01e4043e8 Introduce MinDistance to AreaBeam projectile 2022-11-17 20:51:28 +01:00
Thomas Christlieb
c8665c98a6 fix misclicks through sidebar 2022-11-16 23:22:39 +01:00
62 changed files with 814 additions and 741 deletions

View File

@@ -3,7 +3,7 @@ name: Continuous Integration
on:
push:
pull_request:
branches: [ bleed ]
branches: [ bleed, 'prep-*' ]
jobs:
linux:

View File

@@ -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)

View File

@@ -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 }}

View File

@@ -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 #####################
#

View File

@@ -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

View File

@@ -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)

View File

@@ -83,7 +83,7 @@ namespace OpenRA
public MiniYamlNode Clone()
{
return new MiniYamlNode(Key, Value.Clone());
return new MiniYamlNode(Key, Value.Clone(), Comment, Location);
}
}

View File

@@ -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));
}

View File

@@ -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);
}
}

View File

@@ -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" />

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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>()))

View File

@@ -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];
}

View File

@@ -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);

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>();

View File

@@ -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;
}

View File

@@ -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.InternalName))
{
actorReference.Remove(ownerInit);
actorReference.Add(new OwnerInit(world.WorldActor.Owner));
}
actorReference.Add(new SkipMakeAnimsInit());
actorReference.Add(new SpawnedByMapInit(kv.Key));

View File

@@ -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)
{

View File

@@ -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;
}

View File

@@ -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));
}

View File

@@ -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();

View File

@@ -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;
});

View File

@@ -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)))

View File

@@ -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)

View File

@@ -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()) },

View File

@@ -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;
}
}
}

View File

@@ -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'">

View File

@@ -279,8 +279,6 @@ namespace OpenRA.Platforms.Default
else
windowSize = new Size((int)(surfaceSize.Width / windowScale), (int)(surfaceSize.Height / windowScale));
Console.WriteLine("Using window scale {0:F2}", windowScale);
if (Game.Settings.Game.LockMouseWindow)
GrabWindowMouseFocus();
else
@@ -307,7 +305,32 @@ namespace OpenRA.Platforms.Default
{
SDL.SDL_SetWindowFullscreen(Window, (uint)SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP);
SDL.SDL_SetHint(SDL.SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
if (Platform.CurrentPlatform == PlatformType.OSX)
{
// Activating SDL_WINDOW_FULLSCREEN_DESKTOP on a display with a notch will automatically
// reduce the window height and align the top-left of the window to the safe area.
//
// SDL (as of version 2.26) does not contain an API to query the safeAreaInsets before
// the window is created. We work around this by checking the window height after going
// fullscreen, and recalculating our sizes to match the new window geometry.
//
// This workaround will become redundant once window resizing is implemented.
SDL.SDL_GetWindowSize(Window, out var width, out var height);
if (height != windowSize.Height)
{
windowSize = new Size(width, height);
SDL.SDL_GL_GetDrawableSize(Window, out width, out height);
surfaceSize = new Size(width, height);
windowScale = width * 1f / windowSize.Width;
Console.WriteLine($"Using new resolution: {windowSize.Width}x{windowSize.Height}");
}
}
}
Console.WriteLine($"Using window scale {windowScale:F2}");
}
// Run graphics rendering on a dedicated thread.

View File

@@ -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" />

View File

@@ -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

View File

@@ -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

Binary file not shown.

BIN
mods/d2k/maps/source.oramap Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -27,6 +27,9 @@ AFLD:
ParatroopersPower@paratroopers:
DropItems: E1,E1,E1,E2,E2
HELI:
-MustBeDestroyed:
ATEK:
Buildable:
Prerequisites: ~disabled

View File

@@ -27,6 +27,9 @@ AFLD:
ParatroopersPower@paratroopers:
DropItems: E1,E1,E1,E2,E2
HELI:
-MustBeDestroyed:
ATEK:
Buildable:
Prerequisites: ~disabled

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -48,5 +48,7 @@
<string>{JOIN_SERVER_URL_SCHEME}</string>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
<key>NSPrefersDisplaySafeAreaCompatibilityMode</key>
<false/>
</dict>
</plist>

View 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
View 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],
&params,
&host_context_handle);
hostfxr_run_app(host_context_handle);
return hostfxr_close(host_context_handle);
}

View File

@@ -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"

View 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;
}

View File

@@ -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;
}

View File

@@ -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