Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi gamemod support #2541

Merged
merged 15 commits into from
Dec 22, 2024
4 changes: 2 additions & 2 deletions Content.IntegrationTests/PoolSettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable

using Robust.Shared.Random;

Expand Down Expand Up @@ -93,7 +93,7 @@ public sealed class PoolSettings
/// <summary>
/// Frontier: the preset to run the game in.
/// Set to secret for upstream tests to mimic upstream behaviour.
/// If you need to check adventure game rule things, set this to Adventure.
/// If you need to check adventure game rule things, set this to nfadventure or nfpirate.
/// </summary>
public string GameLobbyDefaultPreset { get; set; } = "secret";

Expand Down
4 changes: 2 additions & 2 deletions Content.Server/GameTicking/Presets/GamePresetPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public sealed partial class GamePresetPrototype : IPrototype
public IReadOnlyList<string> Rules { get; private set; } = Array.Empty<string>();

/// <summary>
/// If specified, the gamemode will only be run with these maps.
/// If none are elligible, the global fallback will be used.
/// If specified, the gamemode will only be run with these maps.
/// If none are elligible, the global fallback will be used.
/// </summary>
[DataField("supportedMaps", customTypeSerializer: typeof(PrototypeIdSerializer<GameMapPoolPrototype>))]
public string? MapPool;
Expand Down
1 change: 0 additions & 1 deletion Content.Server/Shipyard/Systems/ShipyardSystem.Consoles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Content.Server.Radio.EntitySystems;
using Content.Server._NF.Bank;
using Content.Server.Shipyard.Components;
using Content.Shared._NF.GameRule;
using Content.Shared.Bank.Components;
using Content.Shared.Shipyard.Events;
using Content.Shared.Shipyard.BUI;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using Content.Shared.Procedural;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;

namespace Content.Server._NF.GameRule.Components;

[RegisterComponent, Access(typeof(NfAdventureRuleSystem))]
public sealed partial class AdventureRuleComponent : Component
[RegisterComponent, Access(typeof(NFAdventureRuleSystem))]
public sealed partial class NFAdventureRuleComponent : Component
{
public List<EntityUid> NFPlayerMinds = new();
public List<EntityUid> CargoDepots = new();
Expand Down
261 changes: 10 additions & 251 deletions Content.Server/_NF/GameRule/NfAdventureRuleSystem.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
using System.Linq;
using System.Net.Http;
using System.Numerics;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Content.Shared._NF.GameRule;
using Content.Server._NF.GameTicking.Events;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Content.Shared.GameTicking.Components;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Server.Shuttles.Systems;
using Content.Server.Cargo.Components;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.Maps;
using Content.Server.Station.Systems;
using Content.Shared._NF.CCVar; // Frontier
using Robust.Shared.Configuration;
using Content.Shared._NF.Bank;
Expand All @@ -29,26 +20,19 @@
using Content.Shared.GameTicking;
using Robust.Shared.Enums;
using Robust.Server.Player;
using Content.Server.Warps;

namespace Content.Server._NF.GameRule;

/// <summary>
/// This handles the dungeon and trading post spawning, as well as round end capitalism summary
/// </summary>
public sealed class NfAdventureRuleSystem : GameRuleSystem<AdventureRuleComponent>
public sealed class NFAdventureRuleSystem : GameRuleSystem<NFAdventureRuleComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly MetaDataSystem _meta = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly ShuttleSystem _shuttle = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly BankSystem _bank = default!;
[Dependency] private readonly StationRenameWarpsSystems _renameWarps = default!;
[Dependency] private readonly PointOfInterestSystem _poi = default!;

private readonly HttpClient _httpClient = new();

Expand Down Expand Up @@ -77,11 +61,6 @@ public PlayerRoundBankInformation(int startBalance, string name, NetUserId userI
[ViewVariables]
private Dictionary<EntityUid, PlayerRoundBankInformation> _players = new();

private float _distanceOffset = 1f;
private List<Vector2> _stationCoords = new();

private MapId _mapId;

/// <inheritdoc/>
public override void Initialize()
{
Expand All @@ -92,7 +71,7 @@ public override void Initialize()
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
}

protected override void AppendRoundEndText(EntityUid uid, AdventureRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent ev)
protected override void AppendRoundEndText(EntityUid uid, NFAdventureRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent ev)
{
ev.AddLine(Loc.GetString("adventure-list-start"));
var allScore = new List<Tuple<string, int>>();
Expand Down Expand Up @@ -206,12 +185,9 @@ private void OnRoundRestart(RoundRestartCleanupEvent ev)
_players.Clear();
}

protected override void Started(EntityUid uid, AdventureRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
protected override void Started(EntityUid uid, NFAdventureRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
_mapId = GameTicker.DefaultMap;

_distanceOffset = _configurationManager.GetCVar(NFCCVars.POIDistanceModifier);
_stationCoords = new List<Vector2>();
var mapUid = GameTicker.DefaultMap;

//First, we need to grab the list and sort it into its respective spawning logics
List<PointOfInterestPrototype> depotProtos = new();
Expand All @@ -237,235 +213,18 @@ protected override void Started(EntityUid uid, AdventureRuleComponent component,
remainingUniqueProtosBySpawnGroup[location.SpawnGroup].Add(location);
}
}
GenerateDepots(depotProtos, out component.CargoDepots);
GenerateMarkets(marketProtos, out component.MarketStations);
GenerateRequireds(requiredProtos, out component.RequiredPois);
GenerateOptionals(optionalProtos, out component.OptionalPois);
GenerateUniques(remainingUniqueProtosBySpawnGroup, out component.UniquePois);
_poi.GenerateDepots(mapUid, depotProtos, out component.CargoDepots);
_poi.GenerateMarkets(mapUid, marketProtos, out component.MarketStations);
_poi.GenerateRequireds(mapUid, requiredProtos, out component.RequiredPois);
_poi.GenerateOptionals(mapUid, optionalProtos, out component.OptionalPois);
_poi.GenerateUniques(mapUid, remainingUniqueProtosBySpawnGroup, out component.UniquePois);

base.Started(uid, component, gameRule, args);

// Using invalid entity, we don't have a relevant entity to reference here.
RaiseLocalEvent(EntityUid.Invalid, new StationsGeneratedEvent(), broadcast: true); // TODO: attach this to a meaningful entity.
}

private void GenerateDepots(List<PointOfInterestPrototype> depotPrototypes, out List<EntityUid> depotStations)
{
//For depots, we want them to fill a circular type dystance formula to try to keep them as far apart as possible
//Therefore, we will be taking our range properties and treating them as magnitudes of a direction vector divided
//by the number of depots set in our corresponding cvar

depotStations = new List<EntityUid>();
var depotCount = _configurationManager.GetCVar(NFCCVars.CargoDepots);
var rotation = 2 * Math.PI / depotCount;
var rotationOffset = _random.NextAngle() / depotCount;

for (int i = 0; i < depotCount && depotPrototypes.Count > 0; i++)
{
var proto = _random.Pick(depotPrototypes);
Vector2i offset = new Vector2i((int) (_random.Next(proto.MinimumDistance, proto.MaximumDistance) * _distanceOffset), 0);
offset = offset.Rotate(rotationOffset);
rotationOffset += rotation;
// Append letter to depot name.

string overrideName = proto.Name;
if (i < 26)
overrideName += $" {(char) ('A' + i)}"; // " A" ... " Z"
else
overrideName += $" {i + 1}"; // " 27", " 28"...
if (TrySpawnPoiGrid(proto, offset, out var depotUid, overrideName: overrideName) && depotUid is { Valid: true } depot)
{
depotStations.Add(depot);
AddStationCoordsToSet(offset); // adjust list of actual station coords
}
}
}

private void GenerateMarkets(List<PointOfInterestPrototype> marketPrototypes, out List<EntityUid> marketStations)
{
//For market stations, we are going to allow for a bit of randomness and a different offset configuration. We dont
//want copies of this one, since these can be more themed and duplicate names, for instance, can make for a less
//ideal world

marketStations = new List<EntityUid>();
var marketCount = _configurationManager.GetCVar(NFCCVars.MarketStations);
_random.Shuffle(marketPrototypes);
int marketsAdded = 0;
foreach (var proto in marketPrototypes)
{
if (marketsAdded >= marketCount)
break;

var offset = GetRandomPOICoord(proto.MinimumDistance, proto.MaximumDistance, true);

if (TrySpawnPoiGrid(proto, offset, out var marketUid) && marketUid is { Valid: true } market)
{
marketStations.Add(market);
marketsAdded++;
AddStationCoordsToSet(offset);
}
}
}

private void GenerateOptionals(List<PointOfInterestPrototype> optionalPrototypes, out List<EntityUid> optionalStations)
{
//Stations that do not have a defined grouping in their prototype get a default of "Optional" and get put into the
//generic random rotation of POIs. This should include traditional places like Tinnia's rest, the Science Lab, The Pit,
//and most RP places. This will essentially put them all into a pool to pull from, and still does not use the RNG function.

optionalStations = new List<EntityUid>();
var optionalCount = _configurationManager.GetCVar(NFCCVars.OptionalStations);
_random.Shuffle(optionalPrototypes);
int optionalsAdded = 0;
foreach (var proto in optionalPrototypes)
{
if (optionalsAdded >= optionalCount)
break;

var offset = GetRandomPOICoord(proto.MinimumDistance, proto.MaximumDistance, true);

if (TrySpawnPoiGrid(proto, offset, out var optionalUid) && optionalUid is { Valid: true } uid)
{
optionalStations.Add(uid);
AddStationCoordsToSet(offset);
}
}
}

private void GenerateRequireds(List<PointOfInterestPrototype> requiredPrototypes, out List<EntityUid> requiredStations)
{
//Stations are required are ones that are vital to function but otherwise still follow a generic random spawn logic
//Traditionally these would be stations like Expedition Lodge, NFSD station, Prison/Courthouse POI, etc.
//There are no limit to these, and any prototype marked alwaysSpawn = true will get pulled out of any list that isnt Markets/Depots
//And will always appear every time, and also will not be included in other optional/dynamic lists

requiredStations = new List<EntityUid>();
foreach (var proto in requiredPrototypes)
{
var offset = GetRandomPOICoord(proto.MinimumDistance, proto.MaximumDistance, true);

if (TrySpawnPoiGrid(proto, offset, out var requiredUid) && requiredUid is { Valid: true } uid)
{
requiredStations.Add(uid);
AddStationCoordsToSet(offset);
}
}
}

private void GenerateUniques(Dictionary<string, List<PointOfInterestPrototype>> uniquePrototypes, out List<EntityUid> uniqueStations)
{
//Unique locations are semi-dynamic groupings of POIs that rely each independantly on the SpawnChance per POI prototype
//Since these are the remainder, and logically must have custom-designated groupings, we can then know to subdivide
//our random pool into these found groups.
//To do this with an equal distribution on a per-POI, per-round percentage basis, we are going to ensure a random
//pick order of which we analyze our weighted chances to spawn, and if successful, remove every entry of that group
//entirely.

uniqueStations = new List<EntityUid>();
foreach (var prototypeList in uniquePrototypes.Values)
{
// Try to spawn
_random.Shuffle(prototypeList);
foreach (var proto in prototypeList)
{
var chance = _random.NextFloat(0, 1);
if (chance <= proto.SpawnChance)
{
var offset = GetRandomPOICoord(proto.MinimumDistance, proto.MaximumDistance, true);

if (TrySpawnPoiGrid(proto, offset, out var optionalUid) && optionalUid is { Valid: true } uid)
{
uniqueStations.Add(uid);
AddStationCoordsToSet(offset);
break;
}
}
}
}
}

private bool TrySpawnPoiGrid(PointOfInterestPrototype proto, Vector2 offset, out EntityUid? gridUid, string? overrideName = null)
{
gridUid = null;
if (_map.TryLoad(_mapId, proto.GridPath.ToString(), out var mapUids,
new MapLoadOptions
{
Offset = offset,
Rotation = _random.NextAngle()
}))
{

string stationName = string.IsNullOrEmpty(overrideName) ? proto.Name : overrideName;

EntityUid? stationUid = null;
if (_prototypeManager.TryIndex<GameMapPrototype>(proto.ID, out var stationProto))
{
stationUid = _station.InitializeNewStation(stationProto.Stations[proto.ID], mapUids, stationName);
}

foreach (var grid in mapUids)
{
var meta = EnsureComp<MetaDataComponent>(grid);
_meta.SetEntityName(grid, stationName, meta);

EntityManager.AddComponents(grid, proto.AddComponents);
}

// Rename warp points after set up if needed
if (proto.NameWarp)
{
bool? hideWarp = proto.HideWarp ? true : null;
if (stationUid != null)
_renameWarps.SyncWarpPointsToStation(stationUid.Value, forceAdminOnly: hideWarp);
else
_renameWarps.SyncWarpPointsToGrids(mapUids, forceAdminOnly: hideWarp);
}

gridUid = mapUids[0];
return true;
}

return false;
}

private Vector2 GetRandomPOICoord(float unscaledMinRange, float unscaledMaxRange, bool scaleRange)
{
int numRetries = int.Max(_configurationManager.GetCVar(NFCCVars.POIPlacementRetries), 0);
float minDistance = float.Max(_configurationManager.GetCVar(NFCCVars.MinPOIDistance), 0); // Constant at the end to avoid NaN weirdness

Vector2 coords = _random.NextVector2(unscaledMinRange, unscaledMaxRange);
if (scaleRange)
coords *= _distanceOffset;
for (int i = 0; i < numRetries; i++)
{
bool positionIsValid = true;
foreach (var station in _stationCoords)
{
if (Vector2.Distance(station, coords) < minDistance)
{
positionIsValid = false;
break;
}
}

// We have a valid position
if (positionIsValid)
break;

// No vector yet, get next value.
coords = _random.NextVector2(unscaledMinRange, unscaledMaxRange);
if (scaleRange)
coords *= _distanceOffset;
}

return coords;
}

private void AddStationCoordsToSet(Vector2 coords)
{
_stationCoords.Add(coords);
}

private async Task ReportRound(string message, int color = 0x77DDE7)
{
Logger.InfoS("discord", message);
Expand Down
Loading
Loading