Initial commit: OpenRA game engine
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
let5sne.win10
2026-01-10 21:46:54 +08:00
commit 9cf6ebb986
4065 changed files with 635973 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
#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 OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Sound
{
sealed class ActorLostNotificationInfo : ConditionalTraitInfo
{
[NotificationReference("Speech")]
[Desc("Speech notification to play.")]
public readonly string Notification = "UnitLost";
[Desc("Text notification to display.")]
[FluentReference(optional: true)]
public readonly string TextNotification = null;
public readonly bool NotifyAll = false;
public override object Create(ActorInitializer init) { return new ActorLostNotification(this); }
}
sealed class ActorLostNotification : ConditionalTrait<ActorLostNotificationInfo>, INotifyKilled
{
public ActorLostNotification(ActorLostNotificationInfo info)
: base(info) { }
void INotifyKilled.Killed(Actor self, AttackInfo e)
{
if (IsTraitDisabled)
return;
var localPlayer = self.World.LocalPlayer;
if (localPlayer == null || localPlayer.Spectating)
return;
var player = Info.NotifyAll ? localPlayer : self.Owner;
Game.Sound.PlayNotification(self.World.Map.Rules, player, "Speech", Info.Notification, self.Owner.Faction.InternalName);
TextNotificationsManager.AddTransientLine(player, Info.TextNotification);
}
}
}

View File

@@ -0,0 +1,110 @@
#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.Collections.Immutable;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Sound
{
[Desc("Plays a looping audio file at the actor position. Attach this to the `World` actor to cover the whole map.")]
sealed class AmbientSoundInfo : ConditionalTraitInfo
{
[FieldLoader.Require]
public readonly ImmutableArray<string> SoundFiles = default;
[Desc("Initial delay (in ticks) before playing the sound for the first time.",
"Two values indicate a random delay range.")]
public readonly ImmutableArray<int> Delay = [0];
[Desc("Interval between playing the sound (in ticks).",
"Two values indicate a random delay range.")]
public readonly ImmutableArray<int> Interval = [0];
public override object Create(ActorInitializer init) { return new AmbientSound(init.Self, this); }
}
sealed class AmbientSound : ConditionalTrait<AmbientSoundInfo>, ITick, INotifyRemovedFromWorld
{
readonly bool loop;
readonly HashSet<ISound> currentSounds = [];
WPos cachedPosition;
int delay;
public AmbientSound(Actor self, AmbientSoundInfo info)
: base(info)
{
delay = Util.RandomInRange(self.World.SharedRandom, info.Delay);
loop = Info.Interval.Length == 0 || (Info.Interval.Length == 1 && Info.Interval[0] == 0);
}
void ITick.Tick(Actor self)
{
if (IsTraitDisabled)
return;
currentSounds.RemoveWhere(s => s == null || s.Complete);
if (self.OccupiesSpace != null)
{
var pos = self.CenterPosition;
if (pos != cachedPosition)
{
foreach (var s in currentSounds)
s.SetPosition(pos);
cachedPosition = pos;
}
}
if (delay < 0)
return;
if (--delay < 0)
{
StartSound(self);
if (!loop)
delay = Util.RandomInRange(self.World.SharedRandom, Info.Interval);
}
}
void StartSound(Actor self)
{
var sound = Info.SoundFiles.RandomOrDefault(Game.CosmeticRandom);
ISound s;
if (self.OccupiesSpace != null)
{
cachedPosition = self.CenterPosition;
s = loop ? Game.Sound.PlayLooped(SoundType.World, sound, cachedPosition) :
Game.Sound.Play(SoundType.World, sound, self.CenterPosition);
}
else
s = loop ? Game.Sound.PlayLooped(SoundType.World, sound) :
Game.Sound.Play(SoundType.World, sound);
currentSounds.Add(s);
}
void StopSound()
{
foreach (var s in currentSounds)
Game.Sound.StopSound(s);
currentSounds.Clear();
}
protected override void TraitEnabled(Actor self) { delay = Util.RandomInRange(self.World.SharedRandom, Info.Delay); }
protected override void TraitDisabled(Actor self) { StopSound(); }
void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) { StopSound(); }
}
}

View File

@@ -0,0 +1,53 @@
#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 OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Sound
{
[Desc("Play the Kill voice of this actor when eliminating enemies.")]
public class AnnounceOnKillInfo : TraitInfo
{
[Desc("Minimum duration (in milliseconds) between sound events.")]
public readonly int Interval = 5000;
[VoiceReference]
[Desc("Voice to use when killing something.")]
public readonly string Voice = "Kill";
public override object Create(ActorInitializer init) { return new AnnounceOnKill(this); }
}
public class AnnounceOnKill : INotifyAppliedDamage
{
readonly AnnounceOnKillInfo info;
long lastAnnounce;
public AnnounceOnKill(AnnounceOnKillInfo info)
{
this.info = info;
lastAnnounce = -info.Interval;
}
void INotifyAppliedDamage.AppliedDamage(Actor self, Actor damaged, AttackInfo e)
{
// Don't notify suicides
if (e.DamageState == DamageState.Dead && damaged != e.Attacker)
{
if (Game.RunTime > lastAnnounce + info.Interval)
self.PlayVoice(info.Voice);
lastAnnounce = Game.RunTime;
}
}
}
}

View File

@@ -0,0 +1,72 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Sound
{
[Desc("Players will be notified when this actor becomes visible to them.",
"Requires the 'EnemyWatcher' trait on the player actor.")]
public class AnnounceOnSeenInfo : TraitInfo
{
[Desc("Should there be a radar ping on enemies' radar at the actor's location when they see him")]
public readonly bool PingRadar = false;
[NotificationReference("Speech")]
[Desc("Speech notification to play.")]
public readonly string Notification = null;
[FluentReference(optional: true)]
[Desc("Text notification to display.")]
public readonly string TextNotification = null;
public readonly bool AnnounceNeutrals = false;
public override object Create(ActorInitializer init) { return new AnnounceOnSeen(init.Self, this); }
}
public class AnnounceOnSeen : INotifyDiscovered
{
public readonly AnnounceOnSeenInfo Info;
readonly Lazy<RadarPings> radarPings;
public AnnounceOnSeen(Actor self, AnnounceOnSeenInfo info)
{
Info = info;
radarPings = Exts.Lazy(self.World.WorldActor.Trait<RadarPings>);
}
public void OnDiscovered(Actor self, Player discoverer, bool playNotification)
{
if (!playNotification || discoverer != self.World.RenderPlayer)
return;
// Hack to disable notifications for neutral actors so some custom maps don't need fixing
// At this point it's either neutral or an enemy
if (!Info.AnnounceNeutrals && !self.AppearsHostileTo(discoverer.PlayerActor))
return;
// Audio notification
if (discoverer != null && !string.IsNullOrEmpty(Info.Notification))
Game.Sound.PlayNotification(self.World.Map.Rules, discoverer, "Speech", Info.Notification, discoverer.Faction.InternalName);
if (discoverer != null)
TextNotificationsManager.AddTransientLine(discoverer, Info.TextNotification);
// Radar notification
if (Info.PingRadar)
radarPings.Value?.Add(() => true, self.CenterPosition, Color.Red, 50);
}
}
}

View File

@@ -0,0 +1,80 @@
#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.Immutable;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Sound
{
[Desc("Played when preparing for an attack or attacking.")]
public class AttackSoundsInfo : ConditionalTraitInfo
{
[Desc("Play a randomly selected sound from this list when preparing for an attack or attacking.")]
public readonly ImmutableArray<string> Sounds = [];
[Desc("Delay in ticks before sound starts, either relative to attack preparation or attack.")]
public readonly int Delay = 0;
[Desc("Should the sound be delayed relative to preparation or actual attack?")]
public readonly AttackDelayType DelayRelativeTo = AttackDelayType.Preparation;
public override object Create(ActorInitializer init) { return new AttackSounds(this); }
}
public class AttackSounds : ConditionalTrait<AttackSoundsInfo>, INotifyAttack, ITick
{
readonly AttackSoundsInfo info;
int tick;
public AttackSounds(AttackSoundsInfo info)
: base(info)
{
this.info = info;
}
void PlaySound(Actor self)
{
if (info.Sounds.Length > 0)
Game.Sound.Play(SoundType.World, info.Sounds, self.World, self.CenterPosition);
}
void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel)
{
if (info.DelayRelativeTo == AttackDelayType.Attack)
{
if (info.Delay > 0)
tick = info.Delay;
else
PlaySound(self);
}
}
void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel)
{
if (info.DelayRelativeTo == AttackDelayType.Preparation)
{
if (info.Delay > 0)
tick = info.Delay;
else
PlaySound(self);
}
}
void ITick.Tick(Actor self)
{
if (IsTraitDisabled)
return;
if (info.Delay > 0 && --tick == 0)
PlaySound(self);
}
}
}

View File

@@ -0,0 +1,63 @@
#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 OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Sound
{
public class CaptureNotificationInfo : TraitInfo
{
[NotificationReference("Speech")]
[Desc("Speech notification to play to the new owner.")]
public readonly string Notification = "BuildingCaptured";
[FluentReference(optional: true)]
[Desc("Text notification to display to the new owner.")]
public readonly string TextNotification = null;
[Desc("Specifies if Notification is played with the voice of the new owners faction.")]
public readonly bool NewOwnerVoice = true;
[NotificationReference("Speech")]
[Desc("Speech notification to play to the old owner.")]
public readonly string LoseNotification = null;
[FluentReference(optional: true)]
[Desc("Text notification to display to the old owner.")]
public readonly string LoseTextNotification = null;
[Desc("Specifies if LoseNotification is played with the voice of the new owners faction.")]
public readonly bool LoseNewOwnerVoice = false;
public override object Create(ActorInitializer init) { return new CaptureNotification(this); }
}
public class CaptureNotification : INotifyCapture
{
readonly CaptureNotificationInfo info;
public CaptureNotification(CaptureNotificationInfo info)
{
this.info = info;
}
void INotifyCapture.OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner, BitSet<CaptureType> captureTypes)
{
var faction = info.NewOwnerVoice ? newOwner.Faction.InternalName : oldOwner.Faction.InternalName;
Game.Sound.PlayNotification(self.World.Map.Rules, newOwner, "Speech", info.Notification, faction);
TextNotificationsManager.AddTransientLine(newOwner, info.TextNotification);
var loseFaction = info.LoseNewOwnerVoice ? newOwner.Faction.InternalName : oldOwner.Faction.InternalName;
Game.Sound.PlayNotification(self.World.Map.Rules, oldOwner, "Speech", info.LoseNotification, loseFaction);
TextNotificationsManager.AddTransientLine(oldOwner, info.LoseTextNotification);
}
}
}

View File

@@ -0,0 +1,48 @@
#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 OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Sound
{
[Desc("Sounds to play when killed.")]
public class DeathSoundsInfo : ConditionalTraitInfo
{
[VoiceReference]
[Desc("Death notification voice.")]
public readonly string Voice = "Die";
[Desc("Multiply volume with this factor.")]
public readonly float VolumeMultiplier = 1f;
[Desc("Damage types that this should be used for (defined on the warheads).",
"If empty, this will be used as the default sound for all death types.")]
public readonly BitSet<DamageType> DeathTypes = default;
public override object Create(ActorInitializer init) { return new DeathSounds(this); }
}
public class DeathSounds : ConditionalTrait<DeathSoundsInfo>, INotifyKilled
{
public DeathSounds(DeathSoundsInfo info)
: base(info) { }
void INotifyKilled.Killed(Actor self, AttackInfo e)
{
if (IsTraitDisabled)
return;
if (Info.DeathTypes.IsEmpty || e.Damage.DamageTypes.Overlaps(Info.DeathTypes))
self.PlayVoiceLocal(Info.Voice, Info.VolumeMultiplier);
}
}
}

View File

@@ -0,0 +1,60 @@
#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.Immutable;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Sound
{
public class SoundOnDamageTransitionInfo : TraitInfo
{
[Desc("Play a random sound from this list when damaged.")]
public readonly ImmutableArray<string> DamagedSounds = [];
[Desc("Play a random sound from this list when destroyed.")]
public readonly ImmutableArray<string> DestroyedSounds = [];
[Desc("DamageType(s) that trigger the sounds. Leave empty to always trigger a sound.")]
public readonly BitSet<DamageType> DamageTypes = default;
public override object Create(ActorInitializer init) { return new SoundOnDamageTransition(this); }
}
public class SoundOnDamageTransition : INotifyDamageStateChanged
{
readonly SoundOnDamageTransitionInfo info;
public SoundOnDamageTransition(SoundOnDamageTransitionInfo info)
{
this.info = info;
}
void INotifyDamageStateChanged.DamageStateChanged(Actor self, AttackInfo e)
{
if (!info.DamageTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(info.DamageTypes))
return;
var rand = Game.CosmeticRandom;
if (e.DamageState == DamageState.Dead)
{
var sound = info.DestroyedSounds.RandomOrDefault(rand);
Game.Sound.Play(SoundType.World, sound, self.CenterPosition);
}
else if (e.DamageState >= DamageState.Heavy && e.PreviousDamageState < DamageState.Heavy)
{
var sound = info.DamagedSounds.RandomOrDefault(rand);
Game.Sound.Play(SoundType.World, sound, self.CenterPosition);
}
}
}
}

View File

@@ -0,0 +1,62 @@
#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 OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Sound
{
[Desc("Plays a voice clip when the trait is enabled.")]
public class VoiceAnnouncementInfo : ConditionalTraitInfo
{
[VoiceReference]
[FieldLoader.Require]
[Desc("Voice to play.")]
public readonly string Voice = null;
[Desc("Player relationships who can hear this voice.")]
public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Ally | PlayerRelationship.Neutral | PlayerRelationship.Enemy;
[Desc("Play the voice to the owning player even if Stance.Ally is not included in ValidStances.")]
public readonly bool PlayToOwner = true;
[Desc("Disable the announcement after it has been triggered.")]
public readonly bool OneShot = false;
public override object Create(ActorInitializer init) { return new VoiceAnnouncement(this); }
}
public class VoiceAnnouncement : ConditionalTrait<VoiceAnnouncementInfo>
{
bool triggered;
public VoiceAnnouncement(VoiceAnnouncementInfo info)
: base(info) { }
protected override void TraitEnabled(Actor self)
{
if (IsTraitDisabled)
return;
if (Info.OneShot && triggered)
return;
triggered = true;
var player = self.World.LocalPlayer ?? self.World.RenderPlayer;
if (player == null)
return;
if (Info.ValidRelationships.HasRelationship(self.Owner.RelationshipWith(player)))
self.PlayVoice(Info.Voice);
else if (Info.PlayToOwner && self.Owner == player)
self.PlayVoice(Info.Voice);
}
}
}