Compare commits

...

28 Commits

Author SHA1 Message Date
hacker
d40f5292b6 Updated version
Some checks failed
Continuous Integration / Linux (.NET 6.0) (push) Has been cancelled
Continuous Integration / Linux (mono) (push) Has been cancelled
Continuous Integration / Windows (.NET 6.0) (push) Has been cancelled
2024-07-10 00:55:06 +01:00
hacker
3a75add1ba Added Dockerfile 2024-07-10 00:55:04 +01:00
hacker
b3bcdeb4ec Log chat messages on the server 2024-07-10 00:33:09 +01:00
abcdefg30
dde39344a0 Add NotBefore<SpawnStartingUnitsInfo> to LuaScriptInfo
(cherry picked from commit 9f96d0c772)
2023-10-06 15:04:53 +03:00
Pavel Penev
386f691c2e Added a new helper method - a temporary fix
(cherry picked from commit d83e579dfe)
2023-09-27 11:09:34 +03:00
Gustas
24623eaa65 Close the ingame menu upon voting
(cherry picked from commit d5c940ba4c)
2023-09-27 10:47:20 +03:00
Gustas
dc89341634 Add vote kick
(cherry picked from commit 144e716cdf)
2023-09-27 10:47:17 +03:00
JovialFeline
8961b4986f Disable flak truck in Soviet-13, others 2023-09-22 12:26:50 +03:00
Gustas
cbf4207d22 Add backup ExplicitSequenceFilenames to update rules
(cherry picked from commit 29eaab59be)
2023-09-18 11:07:24 +03:00
penev92
c27bf85631 Bumped Eluant NuGet version
The new version fixes the windows 32-bit build not working.
2023-09-16 20:08:05 +02:00
dnqbob
809cb16075 Fix Target.Invalid comparion bug in AutoTarget 2023-09-11 18:57:14 +03:00
Matthias Mailänder
f4c186b7a6 This is not just about difficulty. 2023-08-28 23:34:58 +03:00
Matthias Mailänder
c3cf94b67a The description is optional so don't crash when it is null. 2023-08-28 23:34:58 +03:00
JovialFeline
8b3e7bec2a Add text fix, polish to Controlled Burn 2023-08-28 19:32:56 +02:00
abcdefg30
64ec6eef0a Fix Folder.GetStream using FileNotFoundExceptions to detect if a file exists 2023-08-20 23:01:34 +03:00
dnqbob
4dec1fe430 Autocarryall put down unit if destination is cancelled when picking up 2023-08-19 11:56:35 +03:00
Matthias Mailänder
db3145ed5e Evaluate read only dictionaries. 2023-08-06 17:13:12 +03:00
Gustas
e49135bb09 Fix gen1 map importer crashing on invalid tiles 2023-08-06 13:56:17 +02:00
Gustas
9d79e52989 Fix out of bounds cells not being randomised 2023-08-06 13:56:10 +02:00
Smittytron
0ac9d96ab8 Add Soviet13b 2023-08-06 14:41:15 +03:00
Gustas
5ce559c853 Fix low power notification never triggering 2023-08-05 19:05:52 +02:00
Gustas
a4821b51a2 Grant condition to units closest to the crate 2023-08-05 13:35:55 +02:00
Gustas
cfc026a1ac Fix aircraft jittering 2023-08-05 13:29:41 +02:00
Gustas
58ab3eb153 Fix misaligned TD combat observer tab 2023-08-05 13:23:10 +02:00
Gustas
47b6542b1d Exit game save with escape 2023-08-03 15:56:59 +02:00
Gustas
3c7addcb80 Trigger a button sound when saving a game with enter 2023-08-03 15:56:48 +02:00
Gustas
37f1b9efbf Fix lua sanity check crashing on dedicated servers 2023-08-03 15:34:43 +02:00
abcdefg30
82acdbc32a Fix RA assets installation from the Steam C&C:R version 2023-08-01 22:29:52 +03:00
59 changed files with 2085 additions and 149 deletions

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0
RUN \
apt-get update; \
apt-get -y upgrade; \
apt-get install -y --no-install-recommends \
curl \
wget \
make \
python3 \
unzip \
mono-complete
RUN useradd -d /home/openra -m -s /sbin/nologin openra
WORKDIR /home/openra
COPY . .
RUN chown -R openra:openra .
USER openra
RUN make
EXPOSE 1234
ENTRYPOINT ["./launch-dedicated.sh"]

View File

@@ -562,6 +562,11 @@ namespace OpenRA
{
return new LineSplitEnumerator(str.AsSpan(), separator);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
}
public ref struct LineSplitEnumerator

View File

@@ -41,7 +41,11 @@ namespace OpenRA.FileSystem
public Stream GetStream(string filename)
{
try { return File.OpenRead(Path.Combine(Name, filename)); }
var combined = Path.Combine(Name, filename);
if (!File.Exists(combined))
return null;
try { return File.OpenRead(combined); }
catch { return null; }
}

View File

@@ -86,8 +86,9 @@ namespace OpenRA
static void JoinInner(OrderManager om)
{
// Refresh TextNotificationsManager before the game starts.
// Refresh static classes before the game starts.
TextNotificationsManager.Clear();
UnitOrders.Clear();
// HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game.
// This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining

View File

@@ -20,11 +20,15 @@ namespace OpenRA.Network
{
public const int ChatMessageMaxLength = 2500;
public static int? KickVoteTarget { get; internal set; }
static Player FindPlayerByClient(this World world, Session.Client c)
{
return world.Players.FirstOrDefault(p => p.ClientIndex == c.Index && p.PlayerReference.Playable);
}
static bool OrderNotFromServerOrWorldIsReplay(int clientId, World world) => clientId != 0 || (world != null && world.IsReplay);
internal static void ProcessOrder(OrderManager orderManager, World world, int clientId, Order order)
{
switch (order.OrderString)
@@ -52,9 +56,7 @@ namespace OpenRA.Network
case "DisableChatEntry":
{
// Order must originate from the server
// Don't disable chat in replays
if (clientId != 0 || (world != null && world.IsReplay))
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
// Server may send MaxValue to indicate that it is disabled until further notice
@@ -66,6 +68,26 @@ namespace OpenRA.Network
break;
}
case "StartKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
KickVoteTarget = (int)order.ExtraData;
break;
}
case "EndKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
if (KickVoteTarget == (int)order.ExtraData)
KickVoteTarget = null;
break;
}
case "Chat":
{
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
@@ -365,5 +387,10 @@ namespace OpenRA.Network
if (world.OrderValidators.All(vo => vo.OrderValidation(orderManager, world, clientId, order)))
order.Subject.ResolveOrder(order);
}
public static void Clear()
{
KickVoteTarget = null;
}
}
}

View File

@@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Linguini.Bundle" Version="0.5.0" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.21" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.22" />
<PackageReference Include="Mono.NAT" Version="3.0.4" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />

View File

@@ -148,6 +148,8 @@ namespace OpenRA.Server
GameInformation gameInfo;
readonly List<GameInformation.Player> worldPlayers = new();
readonly Stopwatch pingUpdated = Stopwatch.StartNew();
public readonly VoteKickTracker VoteKickTracker;
readonly PlayerMessageTracker playerMessageTracker;
public ServerState State
@@ -318,6 +320,7 @@ namespace OpenRA.Server
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
VoteKickTracker = new VoteKickTracker(this);
LobbyInfo = new Session
{
@@ -1009,6 +1012,9 @@ namespace OpenRA.Server
case "Chat":
{
var client = GetClient(conn);
Console.WriteLine($"[{DateTime.Now.ToString(Settings.TimestampFormat)}] {client.Name}: {o.TargetString}");
if (Type == ServerType.Local || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
DispatchOrdersToClients(conn, 0, o.Serialize());
@@ -1163,15 +1169,8 @@ namespace OpenRA.Server
return LobbyInfo.ClientWithIndex(conn.PlayerIndex);
}
/// <summary>Does not check if client is admin.</summary>
public bool CanKickClient(Session.Client kickee)
{
if (State != ServerState.GameStarted || kickee.IsObserver)
return true;
var player = worldPlayers.FirstOrDefault(p => p?.ClientIndex == kickee.Index);
return player != null && player.Outcome != WinState.Undefined;
}
public bool HasClientWonOrLost(Session.Client client) =>
worldPlayers.FirstOrDefault(p => p?.ClientIndex == client.Index)?.Outcome != WinState.Undefined;
public void DropClient(Connection toDrop)
{

View File

@@ -0,0 +1,223 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Diagnostics;
using OpenRA.Network;
namespace OpenRA.Server
{
public sealed class VoteKickTracker
{
[TranslationReference("kickee")]
const string InsufficientVotes = "notification-insufficient-votes-to-kick";
[TranslationReference]
const string AlreadyVoted = "notification-kick-already-voted";
[TranslationReference("kicker", "kickee")]
const string VoteKickStarted = "notification-vote-kick-started";
[TranslationReference]
const string UnableToStartAVote = "notification-unable-to-start-a-vote";
[TranslationReference("kickee", "percentage")]
const string VoteKickProgress = "notification-vote-kick-in-progress";
[TranslationReference("kickee")]
const string VoteKickEnded = "notification-vote-kick-ended";
readonly Dictionary<int, bool> voteTracker = new();
readonly Dictionary<Session.Client, long> failedVoteKickers = new();
readonly Server server;
Stopwatch voteKickTimer;
(Session.Client Client, Connection Conn) kickee;
(Session.Client Client, Connection Conn) voteKickerStarter;
public VoteKickTracker(Server server)
{
this.server = server;
}
// Only admins and alive players can participate in a vote kick.
bool ClientHasPower(Session.Client client) => client.IsAdmin || (!client.IsObserver && !server.HasClientWonOrLost(client));
public void Tick()
{
if (voteKickTimer == null)
return;
if (!server.Conns.Contains(kickee.Conn))
{
EndKickVote();
return;
}
if (voteKickTimer.ElapsedMilliseconds > server.Settings.VoteKickTimer)
EndKickVoteAndBlockKicker();
}
public bool VoteKick(Connection conn, Session.Client kicker, Connection kickeeConn, Session.Client kickee, int kickeeID, bool vote)
{
var voteInProgress = voteKickTimer != null;
if (server.State != ServerState.GameStarted
|| (kickee.IsAdmin && server.Type != ServerType.Dedicated)
|| (!voteInProgress && !vote) // Disallow starting a vote with a downvote
|| (voteInProgress && this.kickee.Client != kickee) // Disallow starting new votes when one is already ongoing.
|| !ClientHasPower(kicker))
{
server.SendLocalizedMessageTo(conn, UnableToStartAVote);
return false;
}
short eligiblePlayers = 0;
var isKickeeOnline = false;
var adminIsDeadButOnline = false;
foreach (var c in server.Conns)
{
var client = server.GetClient(c);
if (client != kickee && ClientHasPower(client))
eligiblePlayers++;
if (c == kickeeConn)
isKickeeOnline = true;
if (client.IsAdmin && (client.IsObserver || server.HasClientWonOrLost(client)))
adminIsDeadButOnline = true;
}
if (!isKickeeOnline)
{
EndKickVote();
return false;
}
if (eligiblePlayers < 2 || (adminIsDeadButOnline && !kickee.IsAdmin && eligiblePlayers < 3))
{
if (!kickee.IsObserver && !server.HasClientWonOrLost(kickee))
{
// Vote kick cannot be the sole deciding factor for a game.
server.SendLocalizedMessageTo(conn, InsufficientVotes, Translation.Arguments("kickee", kickee.Name));
EndKickVote();
return false;
}
else if (vote)
{
// If only a single player is playing, allow him to kick observers.
EndKickVote(false);
return true;
}
}
if (!voteInProgress)
{
// Prevent vote kick spam abuse.
if (failedVoteKickers.TryGetValue(kicker, out var time))
{
if (time + server.Settings.VoteKickerCooldown > kickeeConn.ConnectionTimer.ElapsedMilliseconds)
{
server.SendLocalizedMessageTo(conn, UnableToStartAVote);
return false;
}
else
failedVoteKickers.Remove(kicker);
}
Log.Write("server", $"Vote kick started on {kickeeID}.");
voteKickTimer = Stopwatch.StartNew();
server.SendLocalizedMessage(VoteKickStarted, Translation.Arguments("kicker", kicker.Name, "kickee", kickee.Name));
server.DispatchServerOrdersToClients(new Order("StartKickVote", null, false) { ExtraData = (uint)kickeeID }.Serialize());
this.kickee = (kickee, kickeeConn);
voteKickerStarter = (kicker, conn);
}
if (!voteTracker.ContainsKey(conn.PlayerIndex))
voteTracker[conn.PlayerIndex] = vote;
else
{
server.SendLocalizedMessageTo(conn, AlreadyVoted, null);
return false;
}
short votesFor = 0;
short votesAgainst = 0;
foreach (var c in voteTracker)
{
if (c.Value)
votesFor++;
else
votesAgainst++;
}
// Include the kickee in eligeablePlayers, so that in a 2v2 or any other even team
// matchup one team could not vote out the other team's player.
if (ClientHasPower(kickee))
{
eligiblePlayers++;
votesAgainst++;
}
var votesNeeded = eligiblePlayers / 2 + 1;
server.SendLocalizedMessage(VoteKickProgress, Translation.Arguments(
"kickee", kickee.Name,
"percentage", votesFor * 100 / eligiblePlayers));
// If a player or players during a vote lose or disconnect, it is possible that a downvote will
// kick a client. Guard against that situation.
if (vote && (votesFor >= votesNeeded))
{
EndKickVote(false);
return true;
}
// End vote if it can never succeed.
if (eligiblePlayers - votesAgainst < votesNeeded)
{
EndKickVoteAndBlockKicker();
return false;
}
voteKickTimer.Restart();
return false;
}
void EndKickVoteAndBlockKicker()
{
// Make sure vote kick is in progress.
if (voteKickTimer == null)
return;
if (server.Conns.Contains(voteKickerStarter.Conn))
failedVoteKickers[voteKickerStarter.Client] = voteKickerStarter.Conn.ConnectionTimer.ElapsedMilliseconds;
EndKickVote();
}
void EndKickVote(bool sendMessage = true)
{
// Make sure vote kick is in progress.
if (voteKickTimer == null)
return;
if (sendMessage)
server.SendLocalizedMessage(VoteKickEnded, Translation.Arguments("kickee", kickee.Client.Name));
server.DispatchServerOrdersToClients(new Order("EndKickVote", null, false) { ExtraData = (uint)kickee.Client.Index }.Serialize());
voteKickTimer = null;
voteKickerStarter = (null, null);
kickee = (null, null);
voteTracker.Clear();
}
}
}

View File

@@ -114,6 +114,15 @@ namespace OpenRA
[Desc("Delay in milliseconds before players can send chat messages after flood was detected.")]
public int FloodLimitCooldown = 15000;
[Desc("Can players vote to kick other players?")]
public bool EnableVoteKick = true;
[Desc("After how much time in miliseconds should the vote kick fail after idling?")]
public int VoteKickTimer = 30000;
[Desc("If a vote kick was unsuccessful for how long should the player who started the vote not be able to start new votes?")]
public int VoteKickerCooldown = 120000;
public ServerSettings Clone()
{
return (ServerSettings)MemberwiseClone();

View File

@@ -549,7 +549,7 @@ namespace OpenRA.Traits
{
Id = id;
Name = map.GetLocalisedString(name);
Description = map.GetLocalisedString(description);
Description = description != null ? map.GetLocalisedString(description) : null;
IsVisible = visible;
DisplayOrder = displayorder;
Values = values.ToDictionary(v => v.Key, v => map.GetLocalisedString(v.Value));

View File

@@ -103,6 +103,8 @@ namespace OpenRA.Mods.Cnc.UtilityCommands
if (Map.Rules.TerrainInfo is ITerrainInfoNotifyMapCreated notifyMapCreated)
notifyMapCreated.MapCreated(Map);
ReplaceInvalidTerrainTiles(Map);
var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap";
Map.Save(ZipFileLoader.Create(dest));
@@ -159,6 +161,19 @@ namespace OpenRA.Mods.Cnc.UtilityCommands
missionData.Value.Nodes.Add(new MiniYamlNode("Briefing", briefing.Replace("\n", " ").ToString()));
}
static void ReplaceInvalidTerrainTiles(Map map)
{
var terrainInfo = map.Rules.TerrainInfo;
foreach (var uv in map.AllCells.MapCoords)
{
if (!terrainInfo.TryGetTerrainInfo(map.Tiles[uv], out _))
{
map.Tiles[uv] = terrainInfo.DefaultTerrainTile;
Console.WriteLine($"Replaced invalid terrain tile at {uv}");
}
}
}
static void SetBounds(Map map, IniSection mapSection)
{
var offsetX = Exts.ParseIntegerInvariant(mapSection.GetValue("X", "0"));

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Activities;
@@ -185,7 +186,19 @@ namespace OpenRA.Mods.Common.Activities
return true;
var isSlider = aircraft.Info.CanSlide;
var desiredFacing = delta.HorizontalLengthSquared != 0 ? delta.Yaw : aircraft.Facing;
var desiredFacing = aircraft.Facing;
if (delta.HorizontalLengthSquared != 0)
{
var facing = delta.Yaw;
// Prevent jittering.
var diff = Math.Abs(facing.Angle - desiredFacing.Angle);
var deadzone = aircraft.Info.TurnDeadzone.Angle;
if (diff > deadzone && diff < 1024 - deadzone)
desiredFacing = facing;
}
var move = isSlider ? aircraft.FlyStep(desiredFacing) : aircraft.FlyStep(aircraft.Facing);
// Inside the minimum range, so reverse if we CanSlide, otherwise face away from the target.
@@ -194,9 +207,7 @@ namespace OpenRA.Mods.Common.Activities
if (isSlider)
FlyTick(self, aircraft, desiredFacing, aircraft.Info.CruiseAltitude, -move);
else
{
FlyTick(self, aircraft, desiredFacing + new WAngle(512), aircraft.Info.CruiseAltitude, move);
}
return false;
}

View File

@@ -10,32 +10,21 @@
#endregion
using System;
using OpenRA.FileSystem;
using OpenRA.Mods.Common.Scripting;
using OpenRA.Server;
namespace OpenRA.Mods.Common.Lint
{
public class CheckLuaScript : ILintMapPass, ILintServerMapPass
public class CheckLuaScript : ILintMapPass
{
void ILintMapPass.Run(Action<string> emitError, Action<string> emitWarning, ModData modData, Map map)
{
CheckLuaScriptFileExistance(emitError, map.Package, modData.DefaultFileSystem, map.Rules);
}
void ILintServerMapPass.Run(Action<string> emitError, Action<string> emitWarning, ModData modData, MapPreview map, Ruleset mapRules)
{
CheckLuaScriptFileExistance(emitError, map.Package, modData.DefaultFileSystem, mapRules);
}
static void CheckLuaScriptFileExistance(Action<string> emitError, IReadOnlyPackage package, IReadOnlyFileSystem fileSystem, Ruleset mapRules)
{
var luaScriptInfo = mapRules.Actors[SystemActors.World].TraitInfoOrDefault<LuaScriptInfo>();
var luaScriptInfo = map.Rules.Actors[SystemActors.World].TraitInfoOrDefault<LuaScriptInfo>();
if (luaScriptInfo == null)
return;
// We aren't running this lint on servers as they don't create map packages.
foreach (var script in luaScriptInfo.Scripts)
if (!package.Contains(script) && !fileSystem.Exists(script))
if (!map.Package.Contains(script) && !modData.DefaultFileSystem.Exists(script))
emitError($"Lua script `{script}` does not exist.");
}
}

View File

@@ -20,7 +20,7 @@ namespace OpenRA.Mods.Common.Scripting
{
[TraitLocation(SystemActors.World)]
[Desc("Part of the new Lua API.")]
public class LuaScriptInfo : TraitInfo, Requires<SpawnMapActorsInfo>
public class LuaScriptInfo : TraitInfo, Requires<SpawnMapActorsInfo>, NotBefore<SpawnStartingUnitsInfo>
{
[Desc("File names with location relative to the map.")]
public readonly HashSet<string> Scripts = new();

View File

@@ -22,7 +22,7 @@ using S = OpenRA.Server.Server;
namespace OpenRA.Mods.Common.Server
{
public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, INotifyServerEmpty, IClientJoined
public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, INotifyServerEmpty, IClientJoined, OpenRA.Server.ITick
{
[TranslationReference]
const string CustomRules = "notification-custom-rules";
@@ -55,6 +55,9 @@ namespace OpenRA.Mods.Common.Server
const string NoKickGameStarted = "notification-no-kick-game-started";
[TranslationReference("admin", "player")]
const string AdminKicked = "notification-admin-kicked";
[TranslationReference("player")]
const string Kicked = "notification-kicked";
[TranslationReference("admin", "player")]
@@ -156,6 +159,9 @@ namespace OpenRA.Mods.Common.Server
[TranslationReference]
const string YouWereKicked = "notification-you-were-kicked";
[TranslationReference]
const string VoteKickDisabled = "notification-vote-kick-disabled";
readonly IDictionary<string, Func<S, Connection, Session.Client, string, bool>> commandHandlers = new Dictionary<string, Func<S, Connection, Session.Client, string, bool>>
{
{ "state", State },
@@ -170,6 +176,7 @@ namespace OpenRA.Mods.Common.Server
{ "option", Option },
{ "assignteams", AssignTeams },
{ "kick", Kick },
{ "vote_kick", VoteKick },
{ "make_admin", MakeAdmin },
{ "make_spectator", MakeSpectator },
{ "name", Name },
@@ -207,7 +214,7 @@ namespace OpenRA.Mods.Common.Server
lock (server.LobbyInfo)
{
// Kick command is always valid for the host
if (command.StartsWith("kick "))
if (command.StartsWith("kick ", StringComparison.Ordinal) || command.StartsWith("vote_kick ", StringComparison.Ordinal))
return true;
if (server.State == ServerState.GameStarted)
@@ -804,14 +811,14 @@ namespace OpenRA.Mods.Common.Server
return true;
}
if (!server.CanKickClient(kickClient))
if (server.State == ServerState.GameStarted && !kickClient.IsObserver && !server.HasClientWonOrLost(kickClient))
{
server.SendLocalizedMessageTo(conn, NoKickGameStarted);
return true;
}
Log.Write("server", $"Kicking client {kickClientID}.");
server.SendLocalizedMessage(Kicked, Translation.Arguments("admin", client.Name, "player", kickClient.Name));
server.SendLocalizedMessage(AdminKicked, Translation.Arguments("admin", client.Name, "player", kickClient.Name));
server.SendOrderTo(kickConn, "ServerError", YouWereKicked);
server.DropClient(kickConn);
@@ -829,6 +836,62 @@ namespace OpenRA.Mods.Common.Server
}
}
static bool VoteKick(S server, Connection conn, Session.Client client, string s)
{
lock (server.LobbyInfo)
{
var split = s.Split(' ');
if (split.Length != 2)
{
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "vote_kick"));
return true;
}
if (!server.Settings.EnableVoteKick)
{
server.SendLocalizedMessageTo(conn, VoteKickDisabled);
return true;
}
var kickConn = Exts.TryParseInt32Invariant(split[0], out var kickClientID)
? server.Conns.SingleOrDefault(c => server.GetClient(c)?.Index == kickClientID) : null;
if (kickConn == null)
{
server.SendLocalizedMessageTo(conn, KickNone);
return true;
}
var kickClient = server.GetClient(kickConn);
if (client == kickClient)
{
server.SendLocalizedMessageTo(conn, NoKickSelf);
return true;
}
if (!bool.TryParse(split[1], out var vote))
{
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "vote_kick"));
return true;
}
if (server.VoteKickTracker.VoteKick(conn, client, kickConn, kickClient, kickClientID, vote))
{
Log.Write("server", $"Kicking client {kickClientID}.");
server.SendLocalizedMessage(Kicked, Translation.Arguments("player", kickClient.Name));
server.SendOrderTo(kickConn, "ServerError", YouWereKicked);
server.DropClient(kickConn);
server.SyncLobbyClients();
server.SyncLobbySlots();
}
return true;
}
}
void OpenRA.Server.ITick.Tick(S server) => server.VoteKickTracker.Tick();
static bool MakeAdmin(S server, Connection conn, Session.Client client, string s)
{
lock (server.LobbyInfo)

View File

@@ -170,18 +170,15 @@ namespace OpenRA.Mods.Common.Terrain
void ITerrainInfoNotifyMapCreated.MapCreated(Map map)
{
// Randomize PickAny tile variants
// Randomize PickAny tile variants.
var r = new MersenneTwister();
for (var j = map.Bounds.Top; j < map.Bounds.Bottom; j++)
foreach (var uv in map.AllCells.MapCoords)
{
for (var i = map.Bounds.Left; i < map.Bounds.Right; i++)
{
var type = map.Tiles[new MPos(i, j)].Type;
if (!Templates.TryGetValue(type, out var template) || !template.PickAny)
continue;
var type = map.Tiles[uv].Type;
if (!Templates.TryGetValue(type, out var template) || !template.PickAny)
continue;
map.Tiles[new MPos(i, j)] = new TerrainTile(type, (byte)r.Next(0, template.TilesCount));
}
map.Tiles[uv] = new TerrainTile(type, (byte)r.Next(0, template.TilesCount));
}
}
}

View File

@@ -56,6 +56,9 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Turn speed to apply when aircraft flies in circles while idle. Defaults to TurnSpeed if undefined.")]
public readonly WAngle? IdleTurnSpeed = null;
[Desc("When flying if the difference between current facing and desired facing is less than this value, don't turn. This prevents visual jitter.")]
public readonly WAngle TurnDeadzone = new(2);
[Desc("Maximum flight speed when cruising.")]
public readonly int Speed = 1;

View File

@@ -128,10 +128,7 @@ namespace OpenRA.Mods.Common.Traits
return true;
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, carryall.Info.TargetLineColor));
self.QueueActivity(true, new DeliverUnit(self, Target.FromCell(self.World, carryable.Destination ?? self.Location), dropRange, carryall.Info.TargetLineColor));
return true;
}
}

View File

@@ -258,7 +258,7 @@ namespace OpenRA.Mods.Common.Traits
{
var autoTarget = ScanForTarget(self, AllowMove, true);
if (autoTarget != Target.Invalid)
if (autoTarget.Type != TargetType.Invalid)
attacker = autoTarget.Actor;
}

View File

@@ -13,7 +13,7 @@ using System.Linq;
namespace OpenRA.Mods.Common.Traits
{
[Desc("Grants a condition on the collector.")]
[Desc("Grants a condition to the collector and nearby units.")]
public class GrantExternalConditionCrateActionInfo : CrateActionInfo
{
[FieldLoader.Require]
@@ -60,38 +60,36 @@ namespace OpenRA.Mods.Common.Traits
public override void Activate(Actor collector)
{
if (collector.IsInWorld && !collector.IsDead)
GrantCondition(collector);
var actorsInRange = self.World.FindActorsInCircle(self.CenterPosition, info.Range)
.Where(a => a != self && a != collector && a.Owner == collector.Owner && AcceptsCondition(a));
.Where(a => a != self && a != collector && a.IsInWorld && !a.IsDead && a.Owner == collector.Owner && AcceptsCondition(a))
.OrderBy(a => (a.CenterPosition - self.CenterPosition).LengthSquared);
if (info.MaxExtraCollectors > -1)
actorsInRange = actorsInRange.Take(info.MaxExtraCollectors);
collector.World.AddFrameEndTask(w =>
{
foreach (var a in actorsInRange.Append(collector))
{
if (!a.IsInWorld || a.IsDead)
continue;
var externals = a.TraitsImplementing<ExternalCondition>()
.Where(t => t.Info.Condition == info.Condition);
ExternalCondition external = null;
for (var n = 0; n < info.Levels; n++)
{
if (external == null || !external.CanGrantCondition(self))
{
external = externals.FirstOrDefault(t => t.CanGrantCondition(self));
if (external == null)
break;
}
external.GrantCondition(a, self, info.Duration);
}
}
});
foreach (var a in info.MaxExtraCollectors > -1 ? actorsInRange.Take(info.MaxExtraCollectors) : actorsInRange)
GrantCondition(a);
base.Activate(collector);
}
void GrantCondition(Actor actor)
{
var externals = actor.TraitsImplementing<ExternalCondition>()
.Where(t => t.Info.Condition == info.Condition);
ExternalCondition external = null;
for (var n = 0; n < info.Levels; n++)
{
if (external == null || !external.CanGrantCondition(self))
{
external = externals.FirstOrDefault(t => t.CanGrantCondition(self));
if (external == null)
break;
}
external.GrantCondition(actor, self, info.Duration);
}
}
}
}

View File

@@ -68,6 +68,7 @@ namespace OpenRA.Mods.Common.Traits
devMode = self.Trait<DeveloperMode>();
wasHackEnabled = devMode.UnlimitedPower;
PlayLowPowerNotification = info.AdviceInterval > 0;
}
void INotifyCreated.Created(Actor self)

View File

@@ -37,7 +37,7 @@ namespace OpenRA.Mods.Common.Traits
[FieldLoader.Require]
[TranslationReference(dictionaryReference: LintDictionaryReference.Values)]
[Desc("Difficulty levels supported by the map.")]
[Desc("Options to choose from.")]
public readonly Dictionary<string, string> Values = null;
[Desc("Prevent the option from being changed from its default value.")]

View File

@@ -92,19 +92,73 @@ namespace OpenRA.Mods.Common.UpdateRules.Rules
defaultSpriteExtension = defaultSpriteExtensionNode.Value.Value;
}
var tilesetExtensionsNode = spriteSequenceFormatNode.LastChildMatching("TilesetExtensions");
var fromBackup = false;
var tilesetExtensionsNode = spriteSequenceFormatNode.LastChildMatching("TilesetExtensions")?.Value?.Nodes;
if (tilesetExtensionsNode == null)
{
switch (modData.Manifest.Id)
{
case "cnc":
fromBackup = true;
tilesetExtensionsNode = new List<MiniYamlNode>()
{
new MiniYamlNode("TEMPERAT", ".tem"),
new MiniYamlNode("SNOW", ".sno"),
new MiniYamlNode("INTERIOR", ".int"),
new MiniYamlNode("DESERT", ".des"),
new MiniYamlNode("JUNGLE", ".jun"),
};
break;
case "ra":
fromBackup = true;
tilesetExtensionsNode = new List<MiniYamlNode>()
{
new MiniYamlNode("TEMPERAT", ".tem"),
new MiniYamlNode("SNOW", ".sno"),
new MiniYamlNode("INTERIOR", ".int"),
new MiniYamlNode("DESERT", ".des"),
};
break;
case "ts":
fromBackup = true;
tilesetExtensionsNode = new List<MiniYamlNode>()
{
new MiniYamlNode("TEMPERATE", ".tem"),
new MiniYamlNode("SNOW", ".sno"),
};
break;
}
}
if (tilesetExtensionsNode != null)
{
reportModYamlChanges = true;
foreach (var n in tilesetExtensionsNode.Value.Nodes)
if (!fromBackup)
reportModYamlChanges = true;
foreach (var n in tilesetExtensionsNode)
tilesetExtensions[n.Key] = n.Value.Value;
}
var tilesetCodesNode = spriteSequenceFormatNode.LastChildMatching("TilesetCodes");
fromBackup = false;
var tilesetCodesNode = spriteSequenceFormatNode.LastChildMatching("TilesetCodes")?.Value?.Nodes;
if (tilesetCodesNode == null && modData.Manifest.Id == "ts")
{
fromBackup = true;
tilesetCodesNode = new List<MiniYamlNode>()
{
new MiniYamlNode("TEMPERATE", "t"),
new MiniYamlNode("SNOW", "a"),
};
}
if (tilesetCodesNode != null)
{
reportModYamlChanges = true;
foreach (var n in tilesetCodesNode.Value.Nodes)
if (!fromBackup)
reportModYamlChanges = true;
foreach (var n in tilesetCodesNode)
tilesetCodes[n.Key] = n.Value.Value;
}
}

View File

@@ -83,7 +83,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
this.isSavePanel = isSavePanel;
Game.BeforeGameStart += OnGameStart;
panel.Get<ButtonWidget>("CANCEL_BUTTON").OnClick = () =>
var cancelButton = panel.Get<ButtonWidget>("CANCEL_BUTTON");
cancelButton.OnClick = () =>
{
Ui.CloseWindow();
onExit();
@@ -117,17 +118,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic
saveButton.IsVisible = () => true;
var saveWidgets = panel.Get("SAVE_WIDGETS");
saveTextField = saveWidgets.Get<TextFieldWidget>("SAVE_TEXTFIELD");
gameList.Bounds.Height -= saveWidgets.Bounds.Height;
saveWidgets.IsVisible = () => true;
saveTextField.OnEnterKey = _ =>
{
if (!string.IsNullOrWhiteSpace(saveTextField.Text))
Save(world);
return true;
};
saveTextField = saveWidgets.Get<TextFieldWidget>("SAVE_TEXTFIELD");
saveTextField.OnEnterKey = input => saveButton.HandleKeyPress(input);
saveTextField.OnEscKey = input => cancelButton.HandleKeyPress(input);
}
else
{

View File

@@ -40,12 +40,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic
readonly World world;
readonly ModData modData;
readonly Action<bool> hideMenu;
readonly Action closeMenu;
readonly IObjectivesPanel iop;
IngameInfoPanel activePanel;
readonly bool hasError;
[ObjectCreator.UseCtor]
public GameInfoLogic(Widget widget, ModData modData, World world, IngameInfoPanel initialPanel, Action<bool> hideMenu)
public GameInfoLogic(Widget widget, ModData modData, World world, IngameInfoPanel initialPanel, Action<bool> hideMenu, Action closeMenu)
{
var panels = new Dictionary<IngameInfoPanel, (string Panel, string Label, Action<ButtonWidget, Widget> Setup)>()
{
@@ -59,6 +60,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
this.world = world;
this.modData = modData;
this.hideMenu = hideMenu;
this.closeMenu = closeMenu;
activePanel = initialPanel;
var visiblePanels = new List<IngameInfoPanel>();
@@ -140,7 +142,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var panel = hasError ? "SCRIPT_ERROR_PANEL" : iop.PanelName;
Game.LoadWidget(world, panel, objectivesPanelContainer, new WidgetArgs()
{
{ "hideMenu", hideMenu }
{ "hideMenu", hideMenu },
{ "closeMenu", closeMenu },
});
}

View File

@@ -49,6 +49,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
[TranslationReference]
const string Gone = "label-client-state-disconnected";
[TranslationReference]
const string KickTooltip = "button-kick-player";
[TranslationReference("player")]
const string KickTitle = "dialog-kick.title";
@@ -58,8 +61,32 @@ namespace OpenRA.Mods.Common.Widgets.Logic
[TranslationReference]
const string KickAccept = "dialog-kick.confirm";
[TranslationReference]
const string KickVoteTooltip = "button-vote-kick-player";
[TranslationReference("player")]
const string VoteKickTitle = "dialog-vote-kick.title";
[TranslationReference]
const string VoteKickPrompt = "dialog-vote-kick.prompt";
[TranslationReference("bots")]
const string VoteKickPromptBreakBots = "dialog-vote-kick.prompt-break-bots";
[TranslationReference]
const string VoteKickVoteStart = "dialog-vote-kick.vote-start";
[TranslationReference]
const string VoteKickVoteFor = "dialog-vote-kick.vote-for";
[TranslationReference]
const string VoteKickVoteAgainst = "dialog-vote-kick.vote-against";
[TranslationReference]
const string VoteKickVoteCancel = "dialog-vote-kick.vote-cancel";
[ObjectCreator.UseCtor]
public GameInfoStatsLogic(Widget widget, ModData modData, World world, OrderManager orderManager, WorldRenderer worldRenderer, Action<bool> hideMenu)
public GameInfoStatsLogic(Widget widget, ModData modData, World world, OrderManager orderManager, WorldRenderer worldRenderer, Action<bool> hideMenu, Action closeMenu)
{
var player = world.LocalPlayer;
var playerPanel = widget.Get<ScrollPanelWidget>("PLAYER_LIST");
@@ -106,6 +133,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var spectatorTemplate = playerPanel.Get("SPECTATOR_TEMPLATE");
var unmuteTooltip = TranslationProvider.GetString(Unmute);
var muteTooltip = TranslationProvider.GetString(Mute);
var kickTooltip = TranslationProvider.GetString(KickTooltip);
var voteKickTooltip = TranslationProvider.GetString(KickVoteTooltip);
playerPanel.RemoveChildren();
var teams = world.Players.Where(p => !p.NonCombatant && p.Playable)
@@ -114,22 +143,81 @@ namespace OpenRA.Mods.Common.Widgets.Logic
.GroupBy(p => (world.LobbyInfo.ClientWithIndex(p.Player.ClientIndex) ?? new Session.Client()).Team)
.OrderByDescending(g => g.Sum(gg => gg.PlayerStatistics?.Experience ?? 0));
void KickAction(Session.Client client)
void KickAction(Session.Client client, Func<bool> isVoteKick)
{
hideMenu(true);
ConfirmationDialogs.ButtonPrompt(modData,
title: KickTitle,
titleArguments: Translation.Arguments("player", client.Name),
text: KickPrompt,
onConfirm: () =>
if (isVoteKick())
{
var botsCount = 0;
if (client.IsAdmin)
botsCount = world.Players.Count(p => p.IsBot && p.WinState == WinState.Undefined);
if (UnitOrders.KickVoteTarget == null)
{
orderManager.IssueOrder(Order.Command($"kick {client.Index} {false}"));
hideMenu(false);
},
onCancel: () => hideMenu(false),
confirmText: KickAccept);
ConfirmationDialogs.ButtonPrompt(modData,
title: VoteKickTitle,
titleArguments: Translation.Arguments("player", client.Name),
text: botsCount > 0 ? VoteKickPromptBreakBots : VoteKickPrompt,
textArguments: Translation.Arguments("bots", botsCount),
onConfirm: () =>
{
orderManager.IssueOrder(Order.Command($"vote_kick {client.Index} {true}"));
hideMenu(false);
closeMenu();
},
confirmText: VoteKickVoteStart,
onCancel: () => hideMenu(false));
return;
}
ConfirmationDialogs.ButtonPrompt(modData,
title: VoteKickTitle,
titleArguments: Translation.Arguments("player", client.Name),
text: botsCount > 0 ? VoteKickPromptBreakBots : VoteKickPrompt,
textArguments: Translation.Arguments("bots", botsCount),
onConfirm: () =>
{
orderManager.IssueOrder(Order.Command($"vote_kick {client.Index} {true}"));
hideMenu(false);
closeMenu();
},
confirmText: VoteKickVoteFor,
onOther: () =>
{
Ui.CloseWindow();
orderManager.IssueOrder(Order.Command($"vote_kick {client.Index} {false}"));
hideMenu(false);
closeMenu();
},
otherText: VoteKickVoteAgainst,
onCancel: () => hideMenu(false),
cancelText: VoteKickVoteCancel);
}
else
{
ConfirmationDialogs.ButtonPrompt(modData,
title: KickTitle,
titleArguments: Translation.Arguments("player", client.Name),
text: KickPrompt,
onConfirm: () =>
{
orderManager.IssueOrder(Order.Command($"kick {client.Index} {false}"));
hideMenu(false);
},
confirmText: KickAccept,
onCancel: () => hideMenu(false));
}
}
var localClient = orderManager.LocalClient;
var localPlayer = localClient == null ? null : world.Players.FirstOrDefault(player => player.ClientIndex == localClient.Index);
bool LocalPlayerCanKick() => localClient != null
&& (Game.IsHost || ((!orderManager.LocalClient.IsObserver) && localPlayer.WinState == WinState.Undefined));
bool CanClientBeKicked(Session.Client client, Func<bool> isVoteKick) =>
client.Index != localClient.Index && client.State != Session.ClientState.Disconnected
&& (!client.IsAdmin || orderManager.LobbyInfo.GlobalSettings.Dedicated)
&& (!isVoteKick() || UnitOrders.KickVoteTarget == null || UnitOrders.KickVoteTarget == client.Index);
foreach (var t in teams)
{
if (teams.Count() > 1)
@@ -182,8 +270,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
muteCheckbox.GetTooltipText = () => muteCheckbox.IsChecked() ? unmuteTooltip : muteTooltip;
var kickButton = item.Get<ButtonWidget>("KICK");
kickButton.IsVisible = () => Game.IsHost && client.Index != orderManager.LocalClient?.Index && client.State != Session.ClientState.Disconnected && pp.WinState != WinState.Undefined && !pp.IsBot;
kickButton.OnClick = () => KickAction(client);
bool IsVoteKick() => !Game.IsHost || pp.WinState == WinState.Undefined;
kickButton.IsVisible = () => !pp.IsBot && LocalPlayerCanKick() && CanClientBeKicked(client, IsVoteKick);
kickButton.OnClick = () => KickAction(client, IsVoteKick);
kickButton.GetTooltipText = () => IsVoteKick() ? voteKickTooltip : kickTooltip;
playerPanel.AddChild(item);
}
@@ -217,8 +307,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
};
var kickButton = item.Get<ButtonWidget>("KICK");
kickButton.IsVisible = () => Game.IsHost && client.Index != orderManager.LocalClient?.Index && client.State != Session.ClientState.Disconnected;
kickButton.OnClick = () => KickAction(client);
bool IsVoteKick() => !Game.IsHost;
kickButton.IsVisible = () => LocalPlayerCanKick() && CanClientBeKicked(client, IsVoteKick);
kickButton.OnClick = () => KickAction(client, IsVoteKick);
kickButton.GetTooltipText = () => IsVoteKick() ? voteKickTooltip : kickTooltip;
var muteCheckbox = item.Get<CheckboxWidget>("MUTE");
muteCheckbox.IsChecked = () => TextNotificationsManager.MutedPlayers[client.Index];

View File

@@ -200,7 +200,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var gameInfoPanel = Game.LoadWidget(world, "GAME_INFO_PANEL", panelRoot, new WidgetArgs()
{
{ "initialPanel", initialPanel },
{ "hideMenu", requestHideMenu }
{ "hideMenu", requestHideMenu },
{ "closeMenu", CloseMenu },
});
gameInfoPanel.IsVisible = () => !hideMenu;

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits;
@@ -510,8 +511,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{ "PARENT_BOTTOM", parentBounds.Height }
};
var width = w.Width.Evaluate(substitutions);
var height = w.Height.Evaluate(substitutions);
var readOnlySubstitutions = new ReadOnlyDictionary<string, int>(substitutions);
var width = w.Width != null ? w.Width.Evaluate(readOnlySubstitutions) : 0;
var height = w.Height != null ? w.Height.Evaluate(readOnlySubstitutions) : 0;
substitutions.Add("WIDTH", width);
substitutions.Add("HEIGHT", height);
@@ -520,8 +522,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
w.Bounds = new Rectangle(w.Bounds.X, w.Bounds.Y, width, w.Bounds.Height);
else
w.Bounds = new Rectangle(
w.X.Evaluate(substitutions),
w.Y.Evaluate(substitutions),
w.X != null ? w.X.Evaluate(readOnlySubstitutions) : 0,
w.Y != null ? w.Y.Evaluate(readOnlySubstitutions) : 0,
width,
height);

View File

@@ -1 +1 @@
{DEV_VERSION}
release-20231010

View File

@@ -1,6 +1,6 @@
Metadata:
Title: All mods
Version: {DEV_VERSION}
Version: release-20231010
Hidden: true
Packages:

View File

@@ -139,7 +139,6 @@ Container@SKIRMISH_STATS:
Height: 25
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
TooltipText: Kick this player
Children:
Image:
ImageCollection: lobby-bits
@@ -182,7 +181,6 @@ Container@SKIRMISH_STATS:
Height: 25
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
TooltipText: Kick this player
Children:
Image:
ImageCollection: lobby-bits

View File

@@ -1093,14 +1093,14 @@ Container@OBSERVER_WIDGETS:
Align: Right
Shadow: True
Label@ARMY_VALUE:
X: 605
X: 610
Y: 0
Width: 85
Width: 90
Height: PARENT_BOTTOM
Align: Right
Shadow: True
Label@VISION:
X: 690
X: 700
Y: 0
Width: 60
Height: PARENT_BOTTOM

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Tiberian Dawn
Version: {DEV_VERSION}
Version: release-20231010
Website: https://www.openra.net
WebIcon32: https://www.openra.net/images/icons/cnc_32x32.png
WindowTitle: OpenRA - Tiberian Dawn
@@ -37,7 +37,7 @@ Packages:
MapFolders:
cnc|maps: System
~^SupportDir|maps/cnc/{DEV_VERSION}: User
~^SupportDir|maps/cnc/release-20231010: User
Rules:
cnc|rules/misc.yaml

View File

@@ -136,7 +136,6 @@ Container@SKIRMISH_STATS:
Height: 25
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
TooltipText: Kick this player
Children:
Image:
ImageCollection: lobby-bits
@@ -179,7 +178,6 @@ Container@SKIRMISH_STATS:
Height: 25
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
TooltipText: Kick this player
Children:
Image:
ImageCollection: lobby-bits

View File

@@ -33,7 +33,8 @@ notification-admin-change-configuration = Only the host can change the configura
notification-changed-map = { $player } changed the map to { $map }
notification-option-changed = { $player } changed { $name } to { $value }.
notification-you-were-kicked = You have been kicked from the server.
notification-kicked = { $admin } kicked { $player } from the server.
notification-admin-kicked = { $admin } kicked { $player } from the server.
notification-kicked = { $player } was kicked from the server.
notification-temp-ban = { $admin } temporarily banned { $player } from the server.
notification-admin-transfer-admin = Only admins can transfer admin to another player.
notification-admin-move-spectators = Only the host can move players to spectators.
@@ -95,6 +96,14 @@ notification-chat-temp-disabled =
*[other] Chat is disabled. Please try again in { $remaining } seconds.
}
## VoteKickTracker
notification-unable-to-start-a-vote = Unable to start a vote.
notification-insufficient-votes-to-kick = Insufficient votes to kick player { $kickee }.
notification-kick-already-voted = You have already voted.
notification-vote-kick-started = Player { $kicker } has started a vote to kick player { $kickee }.
notification-vote-kick-in-progress = { $percentage }% of players have voted to kick player { $kickee }.
notification-vote-kick-ended = Vote to kick player { $kickee } has failed.
## ActorEditLogic
label-duplicate-actor-id = Duplicate Actor ID
label-actor-id = Enter an Actor ID
@@ -149,12 +158,29 @@ label-mission-failed = Failed
label-client-state-disconnected = Gone
label-mute-player = Mute this player
label-unmute-player = Unmute this player
button-kick-player = Kick this player
button-vote-kick-player = Vote to kick this player
dialog-kick =
.title = Kick { $player }?
.prompt = They will not be able to rejoin this game.
.prompt = This player will not be able to rejoin the game.
.confirm = Kick
dialog-vote-kick =
.title = Vote to kick { $player }?
.prompt = This player will not be able to rejoin the game.
.prompt-break-bots =
{ $bots ->
[one] Kicking the game admin will also kick 1 bot.
*[other] Kicking the game admin will also kick { $bots } bots.
}
.vote-start = Start Vote
.vote-for = Vote For
.vote-against = Vote Against
.vote-cancel = Abstain
notification-vote-kick-disabled = Vote kick is disabled on this server.
## GameTimerLogic
label-paused = Paused
label-max-speed = Max Speed

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Dune 2000
Version: {DEV_VERSION}
Version: release-20231010
Website: https://www.openra.net
WebIcon32: https://www.openra.net/images/icons/d2k_32x32.png
WindowTitle: OpenRA - Dune 2000
@@ -25,7 +25,7 @@ Packages:
MapFolders:
d2k|maps: System
~^SupportDir|maps/d2k/{DEV_VERSION}: User
~^SupportDir|maps/d2k/release-20231010: User
Rules:
d2k|rules/misc.yaml

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Mod Content Manager
Version: {DEV_VERSION}
Version: release-20231010
Hidden: true
Packages:

View File

@@ -234,7 +234,6 @@ cncr-origin: C&C Remastered Collection (Origin version, English)
RegistryValue: Install Dir
IDFiles:
Data/CNCDATA/RED_ALERT/CD1/REDALERT.MIX: 0e58f4b54f44f6cd29fecf8cf379d33cf2d4caef
Length: 4096
# The Remastered Collection doesn't include the RA Soviet CD unfortunately, so we can't install Soviet campaign briefings.
Install:
# Base game files:

View File

@@ -3,7 +3,6 @@ cncr-steam: C&C Remastered Collection (Steam version, English)
AppId: 1213210
IDFiles:
Data/CNCDATA/RED_ALERT/CD1/REDALERT.MIX: 0e58f4b54f44f6cd29fecf8cf379d33cf2d4caef
Length: 4096
# The Remastered Collection doesn't include the RA Soviet CD unfortunately, so we can't install Soviet campaign briefings.
Install:
# Base game files:

View File

@@ -39,6 +39,10 @@ ARTY:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -35,6 +35,10 @@ ARTY:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -33,6 +33,10 @@ powerproxy.paratroopers:
ParatroopersPower:
DropItems: E1,E1,E1,E2,E2
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -41,6 +41,10 @@ powerproxy.paratroopers:
ParatroopersPower:
DropItems: E1,E1,E1,E2,E2
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -42,6 +42,10 @@ MSLO:
Capturable:
Types: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -75,6 +75,10 @@ MSLO:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -282,6 +282,10 @@ STEK:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -100,6 +100,7 @@ GroundWavesDelays =
normal = 3,
hard = 2
}
GroundWavesDelay = GroundWavesDelays[Difficulty]
GroundWaves = function()
if not ForwardCommand.IsDead then
@@ -107,7 +108,7 @@ GroundWaves = function()
local units = Reinforcements.Reinforce(BadGuy, Utils.Random(GroundAttackUnits), path)
Utils.Do(units, IdleHunt)
Trigger.AfterDelay(DateTime.Minutes(GroundWavesDelays), GroundWaves)
Trigger.AfterDelay(DateTime.Minutes(GroundWavesDelay), GroundWaves)
end
end
@@ -132,8 +133,6 @@ ProduceAircraft = function()
end
ActivateAI = function()
GroundWavesDelays = GroundWavesDelays[Difficulty]
local buildings = Utils.Where(Map.ActorsInWorld, function(self) return self.Owner == USSR and self.HasProperty("StartBuildingRepairs") end)
Utils.Do(buildings, function(actor)
Trigger.OnDamaged(actor, function(building)
@@ -146,6 +145,6 @@ ActivateAI = function()
ProduceBadGuyInfantry()
ProduceUSSRInfantry()
ProduceVehicles()
Trigger.AfterDelay(DateTime.Minutes(GroundWavesDelays), GroundWaves)
Trigger.AfterDelay(DateTime.Minutes(GroundWavesDelay), GroundWaves)
Trigger.AfterDelay(DateTime.Minutes(5), ProduceAircraft)
end

View File

@@ -66,14 +66,21 @@ SetupTriggers = function()
Trigger.OnAllKilledOrCaptured(SarinPlants, function()
Greece.MarkCompletedObjective(CaptureSarin)
end)
Trigger.OnEnteredProximityTrigger(AlliesMove.CenterPosition, WDist.FromCells(3), function(actor, id)
if actor.Owner == Greece then
Trigger.RemoveProximityTrigger(id)
Media.PlaySpeechNotification(Greece, "SignalFlareSouth")
end
end)
end
MCVArrived = false
MCVArrivedTick = false
PowerDownTeslas = function()
if not MCVArrived then
CaptureSarin = Greece.AddObjective("capture-sarin-plants-intact")
KillBase = Greece.AddObjective("destroy-enemy-compound")
CaptureSarin = AddPrimaryObjective(Greece, "capture-sarin-plants-intact")
KillBase = AddPrimaryObjective(Greece, "destroy-enemy-compound")
Greece.MarkCompletedObjective(TakeOutPower)
Media.PlaySpeechNotification(Greece, "ReinforcementsArrived")
Reinforcements.Reinforce(Greece, MCVReinforcements[Difficulty], { AlliesSpawn.Location, AlliesMove.Location })
@@ -85,6 +92,7 @@ PowerDownTeslas = function()
Trigger.AfterDelay(DateTime.Seconds(1), function()
MCVArrivedTick = true
PrepareFinishingHunt(USSR)
end)
Trigger.AfterDelay(DateTime.Seconds(60), function()
@@ -99,6 +107,25 @@ PowerDownTeslas = function()
end
end
PrepareFinishingHunt = function(player)
local buildings = GetBaseBuildings(player)
Trigger.OnAllKilledOrCaptured(buildings, function()
Utils.Do(player.GetGroundAttackers(), function(actor)
actor.Stop()
IdleHunt(actor)
end)
end)
end
GetBaseBuildings = function(player)
-- Excludes the unrepairable sarin plants, which is desired anyway.
local buildings = Utils.Where(player.GetActors(), function(actor)
return actor.HasProperty("StartBuildingRepairs")
end)
return buildings
end
Tick = function()
USSR.Cash = 10000
BadGuy.Cash = 10000

View File

@@ -19,6 +19,9 @@ Player:
PlayerResources:
DefaultCash: 7500
HARV:
-MustBeDestroyed:
APC:
Buildable:
Prerequisites: ~vehicles.allies

View File

@@ -42,6 +42,10 @@ MSLO:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,957 @@
MapFormat: 12
RequiresMod: ra
Title: 13b: Capture the Chronosphere
Author: Westwood Studios
Tileset: SNOW
MapSize: 128,128
Bounds: 18,14,92,93
Visibility: MissionSelector
Categories: Campaign
LockPreview: True
Players:
PlayerReference@Neutral:
Name: Neutral
OwnsWorld: True
NonCombatant: True
Faction: england
PlayerReference@Greece:
Name: Greece
Bot: campaign
Faction: allies
Color: E2E6F6
Allies: GoodGuy
Enemies: USSR
PlayerReference@GoodGuy:
Name: GoodGuy
Faction: allies
Color: E2E6F6
Allies: Greece
Enemies: USSR
PlayerReference@USSR:
Name: USSR
AllowBots: False
Playable: True
Required: True
LockFaction: True
Faction: soviet
LockColor: True
Color: FE1100
LockSpawn: True
LockTeam: True
Enemies: GoodGuy, Greece
Actors:
Actor0: cycl
Location: 42,73
Owner: Greece
Actor1: cycl
Location: 43,73
Owner: Greece
Actor2: cycl
Location: 44,73
Owner: Greece
Actor3: cycl
Location: 45,73
Owner: Greece
Actor4: cycl
Location: 46,73
Owner: Greece
Actor5: cycl
Location: 50,73
Owner: Greece
Actor6: cycl
Location: 51,73
Owner: Greece
Actor7: cycl
Location: 52,73
Owner: Greece
Actor8: cycl
Location: 53,73
Owner: Greece
Actor9: cycl
Location: 54,73
Owner: Greece
Actor10: cycl
Location: 55,73
Owner: Greece
Actor11: cycl
Location: 26,74
Owner: Greece
Actor12: cycl
Location: 27,74
Owner: Greece
Actor13: cycl
Location: 28,74
Owner: Greece
Actor14: cycl
Location: 29,74
Owner: Greece
Actor15: cycl
Location: 30,74
Owner: Greece
Actor16: cycl
Location: 35,74
Owner: Greece
Actor17: cycl
Location: 36,74
Owner: Greece
Actor18: cycl
Location: 41,74
Owner: Greece
Actor19: cycl
Location: 42,74
Owner: Greece
Actor20: cycl
Location: 55,74
Owner: Greece
Actor21: cycl
Location: 56,74
Owner: Greece
Actor22: cycl
Location: 25,75
Owner: Greece
Actor23: cycl
Location: 26,75
Owner: Greece
Actor24: cycl
Location: 36,75
Owner: Greece
Actor25: cycl
Location: 37,75
Owner: Greece
Actor26: cycl
Location: 38,75
Owner: Greece
Actor27: cycl
Location: 39,75
Owner: Greece
Actor28: cycl
Location: 40,75
Owner: Greece
Actor29: cycl
Location: 41,75
Owner: Greece
Actor30: cycl
Location: 56,75
Owner: Greece
Actor31: cycl
Location: 57,75
Owner: Greece
Actor32: cycl
Location: 58,75
Owner: Greece
Actor33: cycl
Location: 59,75
Owner: Greece
Actor34: cycl
Location: 25,76
Owner: Greece
Actor35: cycl
Location: 59,76
Owner: Greece
Actor36: cycl
Location: 62,79
Owner: Greece
Actor37: cycl
Location: 63,79
Owner: Greece
Actor38: cycl
Location: 64,79
Owner: Greece
Actor39: cycl
Location: 64,80
Owner: Greece
Actor40: cycl
Location: 65,80
Owner: Greece
Actor41: cycl
Location: 66,80
Owner: Greece
Actor42: cycl
Location: 66,81
Owner: Greece
Actor43: cycl
Location: 67,81
Owner: Greece
Actor44: cycl
Location: 68,81
Owner: Greece
Actor45: cycl
Location: 69,81
Owner: Greece
Actor46: cycl
Location: 66,88
Owner: Greece
Actor47: cycl
Location: 67,88
Owner: Greece
Actor48: cycl
Location: 68,88
Owner: Greece
Actor49: cycl
Location: 69,88
Owner: Greece
Actor50: cycl
Location: 70,88
Owner: Greece
Actor51: cycl
Location: 71,88
Owner: Greece
Actor52: cycl
Location: 71,89
Owner: Greece
Actor53: cycl
Location: 71,90
Owner: Greece
Actor54: cycl
Location: 57,93
Owner: Greece
Actor55: cycl
Location: 60,93
Owner: Greece
Actor56: cycl
Location: 57,94
Owner: Greece
Actor57: cycl
Location: 58,94
Owner: Greece
Actor58: cycl
Location: 59,94
Owner: Greece
Actor59: cycl
Location: 60,94
Owner: Greece
Actor60: brik
Location: 70,94
Owner: Greece
Actor61: brik
Location: 71,94
Owner: Greece
Actor62: brik
Location: 70,95
Owner: Greece
Actor63: brik
Location: 71,95
Owner: Greece
Actor64: cycl
Location: 47,96
Owner: Greece
Actor65: cycl
Location: 48,96
Owner: Greece
Actor66: cycl
Location: 49,96
Owner: Greece
Actor67: cycl
Location: 50,96
Owner: Greece
Actor68: cycl
Location: 47,97
Owner: Greece
Actor69: brik
Location: 26,100
Owner: Greece
Actor70: brik
Location: 27,100
Owner: Greece
Actor71: brik
Location: 28,100
Owner: Greece
Actor72: brik
Location: 29,100
Owner: Greece
Actor73: brik
Location: 70,100
Owner: Greece
Actor74: brik
Location: 71,100
Owner: Greece
Actor75: brik
Location: 26,101
Owner: Greece
Actor76: brik
Location: 29,101
Owner: Greece
Actor77: brik
Location: 70,101
Owner: Greece
Actor78: brik
Location: 71,101
Owner: Greece
Actor79: brik
Location: 26,102
Owner: Greece
Actor80: brik
Location: 29,102
Owner: Greece
Actor81: brik
Location: 26,103
Owner: Greece
Actor82: brik
Location: 27,103
Owner: Greece
Actor83: brik
Location: 28,103
Owner: Greece
Actor84: brik
Location: 29,103
Owner: Greece
Actor85: tc05
Location: 52,104
Owner: Neutral
Actor86: tc02
Location: 46,99
Owner: Neutral
Actor87: t01
Location: 92,88
Owner: Neutral
Actor88: tc04
Location: 76,95
Owner: Neutral
Actor89: tc01
Location: 76,91
Owner: Neutral
Actor90: tc04
Location: 60,65
Owner: Neutral
Actor91: t12
Location: 64,65
Owner: Neutral
Actor92: tc01
Location: 46,56
Owner: Neutral
Actor93: tc05
Location: 53,63
Owner: Neutral
Actor94: tc03
Location: 37,67
Owner: Neutral
Actor95: tc03
Location: 70,59
Owner: Neutral
Actor96: tc02
Location: 87,65
Owner: Neutral
Actor97: t16
Location: 42,48
Owner: Neutral
Actor98: t15
Location: 27,68
Owner: Neutral
Actor99: t14
Location: 56,68
Owner: Neutral
Actor100: t13
Location: 59,50
Owner: Neutral
Actor101: t12
Location: 92,54
Owner: Neutral
Actor102: tc04
Location: 23,63
Owner: Neutral
Actor103: tc02
Location: 18,52
Owner: Neutral
Actor104: tc01
Location: 29,53
Owner: Neutral
Actor105: t15
Location: 27,59
Owner: Neutral
Actor106: tc01
Location: 20,57
Owner: Neutral
Actor107: t16
Location: 19,58
Owner: Neutral
Actor108: t13
Location: 18,57
Owner: Neutral
Actor111: tc04
Location: 56,102
Owner: Neutral
Actor112: tc02
Location: 58,100
Owner: Neutral
Actor114: tc05
Location: 84,33
Owner: Neutral
Actor116: atek
Location: 26,96
Owner: Greece
Chronosphere: pdox
Location: 27,101
Owner: Greece
Actor118: pbox
Location: 48,75
Owner: Greece
Actor119: pbox
Location: 58,80
Owner: Greece
Actor120: pbox
Location: 34,74
Owner: Greece
Actor121: pbox
Location: 31,74
Owner: Greece
Actor122: hbox
Location: 46,99
Owner: Greece
Actor123: hbox
Location: 26,81
Owner: Greece
Actor124: hbox
Location: 26,93
Owner: Greece
Actor125: hbox
Location: 68,89
Owner: Greece
Radar4: dome
Location: 49,104
Owner: Greece
Actor127: gap
Location: 47,105
Owner: Greece
Actor128: gap
Location: 68,91
Owner: Greece
Actor129: gun
Location: 58,78
Owner: Greece
Facing: 892
Actor130: gun
Location: 60,80
Owner: Greece
Facing: 892
Actor131: gun
Location: 50,72
Owner: Greece
Actor132: gun
Location: 46,72
Owner: Greece
Actor133: gun
Location: 72,96
Owner: Greece
Facing: 764
Actor134: gun
Location: 72,99
Owner: Greece
Facing: 764
Actor135: fact
Location: 47,84
Owner: Greece
Actor136: proc
Location: 52,90
Owner: Greece
Actor137: proc
Location: 50,96
Owner: Greece
Actor138: silo
Location: 59,93
Owner: Greece
Actor139: silo
Location: 49,97
Owner: Greece
Actor140: silo
Location: 48,97
Owner: Greece
Actor141: silo
Location: 58,93
Owner: Greece
HPad1: hpad
Location: 32,73
Owner: Greece
HPad4: hpad
Location: 55,96
Owner: Greece
Actor144: apwr
Location: 44,86
Owner: Greece
Actor145: apwr
Location: 42,104
Owner: Greece
Actor146: apwr
Location: 43,98
Owner: Greece
Actor147: apwr
Location: 44,92
Owner: Greece
Actor148: apwr
Location: 27,83
Owner: Greece
Actor149: apwr
Location: 50,86
Owner: Greece
Actor150: apwr
Location: 37,104
Owner: Greece
Actor151: apwr
Location: 31,92
Owner: Greece
GreeceTent2: tent
Location: 19,98
Owner: Greece
GreeceTent1: tent
Location: 46,80
Owner: Greece
Radar1: dome
Location: 29,90
Owner: Greece
Radar2: dome
Location: 48,88
Owner: Greece
Radar3: dome
Location: 69,89
Owner: Greece
HPad2: hpad
Location: 56,76
Owner: Greece
GreeceShipyard: syrd
Location: 73,84
Owner: Greece
GoodGuyShipyard: syrd
Location: 32,43
Owner: GoodGuy
Actor160: brl3
Location: 71,87
Owner: Greece
Actor161: barl
Location: 71,86
Owner: Greece
Actor162: barl
Location: 70,87
Owner: Greece
Actor163: hbox
Location: 26,89
Owner: Greece
HPad3: hpad
Location: 61,80
Owner: Greece
Actor165: gap
Location: 24,46
Owner: GoodGuy
Actor166: gun
Location: 27,42
Owner: Greece
Facing: 892
Actor167: gun
Location: 25,41
Owner: Greece
Facing: 892
Actor168: agun
Location: 28,45
Owner: GoodGuy
Actor169: agun
Location: 23,42
Owner: GoodGuy
Actor170: fact
Location: 20,42
Owner: GoodGuy
GoodGuyWarFactory: weap
Location: 25,50
Owner: GoodGuy
Actor172: apwr
Location: 25,47
Owner: GoodGuy
Actor173: apwr
Location: 18,46
Owner: GoodGuy
Actor174: agun
Location: 24,50
Owner: GoodGuy
Actor175: apwr
Location: 27,79
Owner: Greece
GreeceWarFactory: weap
Location: 59,85
Owner: Greece
Actor177: agun
Location: 47,101
Owner: Greece
Actor178: v19
Location: 26,78
Owner: Greece
Actor179: brl3
Location: 26,77
Owner: Greece
Actor180: brl3
Location: 54,97
Owner: Greece
Actor181: barl
Location: 53,97
Owner: Greece
Actor182: v19
Location: 54,96
Owner: Greece
Actor183: brl3
Location: 53,96
Owner: Greece
Actor184: gun
Location: 63,78
Owner: Greece
Actor190: gap
Location: 30,88
Owner: Greece
StartAttack3: 2tnk
Location: 75,60
Owner: Greece
Facing: 892
StartAttack4: 2tnk
Location: 76,62
Owner: Greece
Facing: 892
StartAttack5: 2tnk
Location: 78,62
Owner: Greece
Facing: 892
StartAttack2: arty
Location: 80,59
Owner: Greece
Facing: 892
StartAttack1: arty
Location: 79,58
Owner: Greece
Facing: 892
V2A: v2rl
Location: 84,20
Owner: USSR
Facing: 508
V2B: v2rl
Location: 82,20
Owner: USSR
Facing: 508
Actor199: 4tnk
Location: 80,20
Owner: USSR
Facing: 508
Actor200: 4tnk
Location: 86,20
Owner: USSR
Facing: 508
Actor201: 3tnk
Location: 81,21
Owner: USSR
Facing: 508
Actor202: 3tnk
Location: 85,21
Owner: USSR
Facing: 508
Actor203: 2tnk
Location: 28,41
Owner: Greece
Actor204: 2tnk
Location: 30,41
Owner: Greece
Actor205: 2tnk
Location: 24,40
Owner: Greece
Facing: 892
Actor206: 2tnk
Location: 26,40
Owner: Greece
Facing: 892
Actor207: arty
Location: 29,42
Owner: Greece
Actor208: arty
Location: 24,41
Owner: Greece
Facing: 892
OreAttack3: 2tnk
Location: 39,16
Owner: GoodGuy
Facing: 764
OreAttack5: 2tnk
Location: 40,17
Owner: GoodGuy
Facing: 764
OreAttack4: 2tnk
Location: 41,16
Owner: GoodGuy
Facing: 764
OreAttack2: 2tnk
Location: 40,15
Owner: GoodGuy
Facing: 764
OreAttack6: arty
Location: 40,18
Owner: GoodGuy
Facing: 764
OreAttack1: arty
Location: 40,14
Owner: GoodGuy
Facing: 764
Actor215: 2tnk
Location: 68,97
Owner: Greece
Facing: 764
Actor216: 2tnk
Location: 68,98
Owner: Greece
Facing: 764
Actor217: 2tnk
Location: 67,96
Owner: Greece
Facing: 764
Actor218: 2tnk
Location: 67,99
Owner: Greece
Facing: 764
Actor221: e3
Location: 67,91
Owner: Greece
SubCell: 1
Actor222: e3
Location: 67,89
Owner: Greece
SubCell: 0
Actor223: e3
Location: 71,89
Owner: Greece
SubCell: 1
Actor224: e3
Location: 56,93
Owner: Greece
SubCell: 2
Actor225: e3
Location: 51,106
Owner: Greece
SubCell: 1
Actor226: e3
Location: 48,106
Owner: Greece
SubCell: 2
Actor227: e3
Location: 55,75
Owner: Greece
SubCell: 0
Actor228: e3
Location: 55,76
Owner: Greece
SubCell: 2
Actor229: e3
Location: 63,80
Owner: Greece
SubCell: 2
Actor230: e3
Location: 63,81
Owner: Greece
SubCell: 2
Actor231: e3
Location: 30,75
Owner: Greece
SubCell: 4
Actor232: e3
Location: 31,76
Owner: Greece
SubCell: 2
Actor233: e3
Location: 35,75
Owner: Greece
SubCell: 3
Actor234: e3
Location: 35,76
Owner: Greece
SubCell: 1
Actor235: e3
Location: 46,79
Owner: Greece
SubCell: 3
Actor236: e3
Location: 47,79
Owner: Greece
SubCell: 3
Actor237: e3
Location: 45,85
Owner: Greece
SubCell: 2
Actor238: e3
Location: 45,85
Owner: Greece
SubCell: 1
Actor239: e3
Location: 50,85
Owner: Greece
SubCell: 1
Actor240: e3
Location: 51,85
Owner: Greece
SubCell: 1
Actor241: e3
Location: 47,88
Owner: Greece
SubCell: 2
Actor242: e3
Location: 50,89
Owner: Greece
SubCell: 2
Actor243: e3
Location: 28,90
Owner: Greece
SubCell: 3
Actor244: e3
Location: 28,92
Owner: Greece
SubCell: 1
Actor245: e3
Location: 32,91
Owner: Greece
SubCell: 0
Actor246: dd
Location: 92,101
Owner: Greece
Facing: 892
Actor247: dd
Location: 38,24
Owner: Greece
Facing: 764
Actor248: pt
Location: 92,97
Owner: Greece
Facing: 892
Actor249: pt
Location: 97,101
Owner: Greece
Facing: 892
Actor250: pt
Location: 40,27
Owner: Greece
Facing: 764
Actor251: pt
Location: 41,21
Owner: Greece
Facing: 764
Actor252: pt
Location: 82,82
Owner: Greece
Facing: 508
Actor253: pt
Location: 82,87
Owner: Greece
Actor254: dd
Location: 80,81
Owner: Greece
Facing: 636
Actor255: dd
Location: 80,87
Owner: Greece
Facing: 892
Actor256: dd
Location: 35,41
Owner: GoodGuy
Actor257: dd
Location: 38,46
Owner: GoodGuy
Facing: 892
Actor258: pt
Location: 36,40
Owner: GoodGuy
Actor259: pt
Location: 39,44
Owner: GoodGuy
Facing: 892
Actor260: pt
Location: 35,39
Owner: GoodGuy
Actor273: fix
Owner: Greece
Location: 38,79
Jeep2: jeep
Owner: Greece
Location: 25,91
Facing: 384
Jeep1: jeep
Owner: Greece
Location: 24,91
Facing: 384
Actor277: arty
Owner: Greece
Location: 31,88
Facing: 951
Actor278: arty
Owner: Greece
Location: 34,94
Facing: 919
Actor279: arty
Owner: Greece
Location: 45,101
Facing: 1023
Actor280: arty
Owner: Greece
Location: 50,102
Facing: 991
Actor272: mine
Owner: Neutral
Location: 99,23
Actor274: mine
Owner: Neutral
Location: 64,21
Actor275: mine
Owner: Neutral
Location: 84,74
Actor276: mine
Owner: Neutral
Location: 84,92
DefaultCameraPosition: waypoint
Location: 83,20
Owner: Neutral
MCVEntry: waypoint
Location: 83,14
Owner: Neutral
EastWaterEntry: waypoint
Location: 91,106
Owner: Neutral
EastBeach1: waypoint
Location: 102,51
Owner: Neutral
EastBeach2: waypoint
Location: 103,19
Owner: Neutral
EastBeach3: waypoint
Location: 90,73
Owner: Neutral
WestWaterEntry: waypoint
Location: 18,22
Owner: Neutral
WestBeach1: waypoint
Location: 30,29
Owner: Neutral
WestBeach2: waypoint
Location: 36,17
Owner: Neutral
JeepWaypoint1: waypoint
Location: 23,47
Owner: Neutral
JeepWaypoint2: waypoint
Location: 25,91
Owner: Neutral
ChinookLZ: waypoint
Location: 51,16
Owner: Neutral
BridgeAttackProxy: waypoint
Location: 78,60
Owner: Neutral
ChronoshiftPoint: waypoint
Location: 32,101
Owner: Neutral
WarFactoryRally: waypoint
Location: 56,82
Owner: Neutral
CruiserStop: waypoint
Location: 52,25
Owner: Neutral
Rules: ra|rules/campaign-rules.yaml, ra|rules/campaign-tooltips.yaml, ra|rules/campaign-palettes.yaml, rules.yaml
Translations: ra|languages/lua/en.ftl, ra|languages/difficulties/en.ftl

View File

@@ -0,0 +1,83 @@
World:
LuaScript:
Scripts: campaign.lua, soviet13b.lua, soviet13b-AI.lua, utils.lua
MissionData:
BriefingVideo: soviet13.vqa
WinVideo: sovtstar.vqa
LossVideo: allymorf.vqa
StartVideo: mtnkfact.vqa
Briefing: We have another chance to capture the Chronosphere. Take out the Radar Domes to cut the link between them and the Chronosphere. Then capture it!
ScriptLobbyDropdown@difficulty:
ID: difficulty
Label: dropdown-difficulty.label
Description: dropdown-difficulty.description
Values:
easy: options-difficulty.easy
normal: options-difficulty.normal
hard: options-difficulty.hard
Default: normal
Player:
PlayerResources:
DefaultCash: 10000
AFLD:
AirstrikePower@parabombs:
Prerequisites: aircraft.soviet
ParatroopersPower@paratroopers:
DropItems: E1,E1,E1,E2,E2
PDOX:
Power:
Amount: 0
-WithColoredOverlay@IDISABLE:
Buildable:
Prerequisites: ~disabled
ATEK:
GpsPower:
DisplayTimerRelationships: Ally
MSLO:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled
MECH:
Buildable:
Prerequisites: ~disabled
IRON:
Buildable:
Prerequisites: ~disabled
QTNK:
Buildable:
Prerequisites: ~disabled
E7:
Buildable:
Prerequisites: ~disabled
E7.noautotarget:
Buildable:
Prerequisites: ~disabled
E3:
Buildable:
Prerequisites: ~tent
MSUB:
Buildable:
Prerequisites: ~disabled
THF:
Buildable:
Prerequisites: ~disabled

View File

@@ -0,0 +1,170 @@
--[[
Copyright (c) The OpenRA Developers and Contributors
This file is part of OpenRA, which is free software. It is made
available to you under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version. For more
information, see COPYING.
]]
WTransWays =
{
{ EastWaterEntry.Location, EastBeach1.Location },
{ EastWaterEntry.Location, EastBeach2.Location },
{ EastWaterEntry.Location, EastBeach3.Location },
{ WestWaterEntry.Location, WestBeach1.Location },
{ WestWaterEntry.Location, WestBeach2.Location }
}
WTransUnits =
{
hard = { { "2tnk", "2tnk", "e3", "e3", "e3" }, { "2tnk", "2tnk", "2tnk", "2tnk" } },
normal = { { "2tnk", "1tnk", "e3", "e3", "jeep" }, { "2tnk", "2tnk", "1tnk", "jeep" } },
easy = { { "2tnk", "e1", "e1", "e3", "e3" }, { "1tnk", "1tnk", "jeep", "jeep" } }
}
WTransDelays =
{
easy = DateTime.Minutes(5),
normal = DateTime.Minutes(4),
hard = DateTime.Minutes(3)
}
AttackGroup = { }
AttackGroupSize = 8
ProductionInterval =
{
easy = DateTime.Seconds(20),
normal = DateTime.Seconds(14),
hard = DateTime.Seconds(8)
}
AlliedInfantry = { "e1", "e3" }
AlliedVehiclesUpgradeDelay = DateTime.Minutes(8)
AlliedVehicleType = "Normal"
AlliedVehicles =
{
Normal = { "1tnk", "2tnk", "2tnk" },
Upgraded = { "2tnk", "2tnk", "arty" }
}
WTransWaves = function()
local way = Utils.Random(WTransWays)
local units = Utils.Random(WTransUnits)
local attackUnits = Reinforcements.ReinforceWithTransport(Greece, "lst", units , way, { way[2], way[1] })[2]
Utils.Do(attackUnits, function(a)
Trigger.OnAddedToWorld(a, function()
IdleHunt(a)
end)
end)
Trigger.AfterDelay(WTransDelay, WTransWaves)
end
SendAttackGroup = function()
if #AttackGroup < AttackGroupSize then
return
end
Utils.Do(AttackGroup, IdleHunt)
AttackGroup = { }
end
ProduceInfantry = function()
if (GreeceTent1.IsDead or GreeceTent1.Owner ~= Greece) and (GreeceTent2.IsDead or GreeceTent2.Owner ~= Greece) then
return
end
Greece.Build({ Utils.Random(AlliedInfantry) }, function(units)
table.insert(AttackGroup, units[1])
SendAttackGroup()
Trigger.AfterDelay(ProductionInterval[Difficulty], ProduceInfantry)
end)
end
ProduceVehicles = function()
if GreeceWarFactory.IsDead or GreeceWarFactory.Owner ~= Greece then
return
end
Greece.Build({ Utils.Random(AlliedVehicles[AlliedVehicleType]) }, function(units)
table.insert(AttackGroup, units[1])
SendAttackGroup()
Trigger.AfterDelay(ProductionInterval[Difficulty], ProduceVehicles)
end)
end
AlliedAircraftType = { "heli" }
Longbows = { }
AlliedAircraft = function()
if (HPad1.IsDead or HPad1.Owner ~= Greece) and (HPad2.IsDead or HPad2.Owner ~= Greece) and (HPad3.IsDead or HPad3.Owner ~= Greece) and (HPad4.IsDead or HPad4.Owner ~= Greece) then
return
end
Greece.Build(AlliedAircraftType, function(units)
local longbow = units[1]
Longbows[#Longbows + 1] = longbow
Trigger.OnKilled(longbow, AlliedAircraft)
local alive = Utils.Where(Longbows, function(y) return not y.IsDead end)
if #alive < 2 then
Trigger.AfterDelay(DateTime.Seconds(75), AlliedAircraft)
end
InitializeAttackAircraft(longbow, USSR)
end)
end
SendCruiser = function()
if GoodGuyShipyard.IsDead or GoodGuyShipyard.Owner ~= GoodGuy then
return
end
local boat = Reinforcements.Reinforce(Greece, { "ca" }, { WestWaterEntry.Location })
Utils.Do(boat, function(ca)
ca.Move(CruiserStop.Location)
Trigger.OnKilled(ca, function()
Trigger.AfterDelay(DateTime.Minutes(6), SendCruiser)
end)
end)
end
ChinookChalk = { "e1", "e1", "e1", "e3", "e3", "e3", "e3", "e3" }
ChinookPath = { WestWaterEntry.Location, ChinookLZ.Location }
SendChinook = function()
if (HPad1.IsDead or HPad1.Owner ~= Greece) and (HPad2.IsDead or HPad2.Owner ~= Greece) and (HPad3.IsDead or HPad3.Owner ~= Greece) and (HPad4.IsDead or HPad4.Owner ~= Greece) then
return
end
local chalk = Reinforcements.ReinforceWithTransport(Greece, "tran", ChinookChalk , ChinookPath, { ChinookPath[1] })[2]
Utils.Do(chalk, function(unit)
Trigger.OnAddedToWorld(unit, IdleHunt)
end)
Trigger.AfterDelay(DateTime.Minutes(5), SendChinook)
end
ActivateAI = function()
WTransUnits = WTransUnits[Difficulty]
WTransDelay = WTransDelays[Difficulty]
local buildings = Utils.Where(Map.ActorsInWorld, function(self) return self.Owner == Greece and self.HasProperty("StartBuildingRepairs") end)
Utils.Do(buildings, function(actor)
Trigger.OnDamaged(actor, function(building)
if building.Owner == Greece and building.Health < building.MaxHealth * 3/4 then
building.StartBuildingRepairs()
end
end)
end)
Trigger.AfterDelay(AlliedVehiclesUpgradeDelay, function() AlliedVehicleType = "Upgraded" end)
ProduceInfantry()
Trigger.AfterDelay(DateTime.Minutes(3), ProduceVehicles)
Trigger.AfterDelay(DateTime.Minutes(5), AlliedAircraft)
Trigger.AfterDelay(DateTime.Minutes(6), WTransWaves)
Trigger.AfterDelay(DateTime.Minutes(10), SendCruiser)
end

View File

@@ -0,0 +1,125 @@
--[[
Copyright (c) The OpenRA Developers and Contributors
This file is part of OpenRA, which is free software. It is made
available to you under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version. For more
information, see COPYING.
]]
Jeeps = { Jeep1, Jeep2 }
JeepWaypoints = { JeepWaypoint1.Location, JeepWaypoint2.Location }
OreAttackers = { OreAttack1, OreAttack2, OreAttack3, OreAttack4, OreAttack5, OreAttack6 }
RadarSites = { Radar1, Radar2, Radar3, Radar4 }
StartAttack = { StartAttack1, StartAttack2, StartAttack3, StartAttack4, StartAttack5 }
ChronoDemolitionTrigger = { CPos.New(36,96), CPos.New(37,96), CPos.New(37,97), CPos.New(38,97), CPos.New(38,98), CPos.New(39,98) }
OreAttackFootprintTrigger = { CPos.New(57,20), CPos.New(57,19), CPos.New(57,18), CPos.New(57,17), CPos.New(57,16), CPos.New(57,15), CPos.New(57,14) }
Start = function()
Reinforcements.Reinforce(USSR, { "mcv" }, { MCVEntry.Location, DefaultCameraPosition.Location }, 5)
Utils.Do(Jeeps, function(jeep)
jeep.Patrol(JeepWaypoints, true, 125)
end)
Utils.Do(StartAttack, function(a)
IdleHunt(a)
end)
ChronoCam = Actor.Create("camera", true, { Owner = USSR, Location = Chronosphere.Location})
end
MissionTriggers = function()
Trigger.OnAllKilled(RadarSites, function()
USSR.MarkCompletedObjective(TakeDownRadar)
ChronoshiftAlliedUnits()
end)
Trigger.OnCapture(Chronosphere, function()
if not USSR.IsObjectiveCompleted(TakeDownRadar) then
Media.DisplayMessage(UserInterface.Translate("chrono-trap-triggered"), UserInterface.Translate("headquarters"))
Chronosphere.Kill()
else
USSR.MarkCompletedObjective(CaptureChronosphere)
end
end)
Trigger.OnKilled(Chronosphere, function()
USSR.MarkFailedObjective(CaptureChronosphere)
end)
local chronoTriggered
Trigger.OnEnteredFootprint(ChronoDemolitionTrigger, function(actor, id)
if actor.Owner == USSR and not chronoTriggered and not USSR.IsObjectiveCompleted(TakeDownRadar) then
Trigger.RemoveFootprintTrigger(id)
Media.DisplayMessage(UserInterface.Translate("chrono-trap-triggered"), UserInterface.Translate("headquarters"))
chronoTriggered = true
Chronosphere.Kill()
end
end)
Trigger.OnEnteredProximityTrigger(ChinookLZ.CenterPosition, WDist.FromCells(5), function(actor, id)
if actor.Owner == USSR and actor.Type == "harv" then
Trigger.RemoveProximityTrigger(id)
SendChinook()
end
end)
local oreAttackTriggered
Trigger.OnEnteredFootprint(OreAttackFootprintTrigger, function(actor, id)
if actor.Owner == USSR and not oreAttackTriggered then
Trigger.RemoveProximityTrigger(id)
oreAttackTrigger = true
Utils.Do(OreAttackers, function(a)
if not a.IsDead then
IdleHunt(a)
end
end)
end
end)
end
ChronoshiftAlliedUnits = function()
if Chronosphere.IsDead then
return
end
local cells = Utils.ExpandFootprint({ ChronoshiftPoint.Location }, false)
local units = { }
for i = 1, #cells do
local unit = Actor.Create("2tnk", true, { Owner = Greece, Facing = Angle.North })
units[unit] = cells[i]
IdleHunt(unit)
end
Chronosphere.Chronoshift(units)
end
Tick = function()
Greece.Cash = 20000
if USSR.HasNoRequiredUnits() then
Greece.MarkCompletedObjective(AlliesObjective)
end
end
WorldLoaded = function()
USSR = Player.GetPlayer("USSR")
Greece = Player.GetPlayer("Greece")
GoodGuy = Player.GetPlayer("GoodGuy")
InitObjectives(USSR)
AlliesObjective = AddPrimaryObjective(Greece, "")
TakeDownRadar = AddPrimaryObjective(USSR, "destroy-allied-radar-sites")
CaptureChronosphere = AddPrimaryObjective(USSR, "capture-the-chronosphere")
Camera.Position = DefaultCameraPosition.CenterPosition
Start()
MissionTriggers()
ActivateAI()
GreeceWarFactory.RallyPoint = WarFactoryRally.Location
if Difficulty == "hard" then
V2A.Destroy()
V2B.Destroy()
end
end

View File

@@ -33,6 +33,7 @@ Soviet Campaign:
soviet-11a
soviet-11b
soviet-13a
soviet-13b
Counterstrike Allied Missions:
sarin-gas-1-crackdown
sarin-gas-2-down-under

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Red Alert
Version: {DEV_VERSION}
Version: release-20231010
Website: https://www.openra.net
WebIcon32: https://www.openra.net/images/icons/ra_32x32.png
WindowTitle: OpenRA - Red Alert
@@ -40,7 +40,7 @@ Packages:
MapFolders:
ra|maps: System
~^SupportDir|maps/ra/{DEV_VERSION}: User
~^SupportDir|maps/ra/release-20231010: User
Rules:
ra|rules/misc.yaml

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Tiberian Sun
Version: {DEV_VERSION}
Version: release-20231010
Website: https://www.openra.net
WebIcon32: https://www.openra.net/images/icons/ts_32x32.png
WindowTitle: OpenRA - Tiberian Sun
@@ -52,7 +52,7 @@ Packages:
MapFolders:
ts|maps: System
~^SupportDir|maps/ts/{DEV_VERSION}: User
~^SupportDir|maps/ts/release-20231010: User
Rules:
ts|rules/ai.yaml