Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ e2e-build: link-libs
@set -eu; for dll in VGModAPI VGModAPI.Core VGModAPI.Abstractions VGModAPI.Unity; do cp "VGModAPI/bin/$(CONFIGURATION)/netstandard2.1/$$dll.dll" "$(E2E_BUILD)/"; done
cp VGModAPI.E2E/bin/$(CONFIGURATION)/netstandard2.1/VGModAPI.E2E.dll "$(E2E_BUILD)/"
cp VGModAPI.E2E/bin/$(CONFIGURATION)/netstandard2.1/Newtonsoft.Json.dll "$(E2E_BUILD)/"
@set -eu; for dll in PocketWorlds CargoRecovery StoryMissions StationCommerce StationCommerceB UiSurfaces Observation; do \
@set -eu; for dll in PocketWorlds CargoRecovery StoryMissions StationCommerce StationCommerceB UiSurfaces Observation EquipmentTargeting; do \
f=$$(find examples -path '*/bin/$(CONFIGURATION)/netstandard2.1/'"$$dll"'.dll' | head -n 1); \
if [ -n "$$f" ]; then cp "$$f" "$(E2E_BUILD)/"; else echo "e2e staging missing example dll: $$dll" >&2; exit 1; fi; \
done
e2e: e2e-build
$(E2E_PYTHON) tools/e2e.py --game-dir '$(call E2E_PATH,$(GAME_DIR))' --build-dir '$(call E2E_PATH,$(E2E_BUILD))' --runtime-dir '$(call E2E_PATH,$(E2E_RUNTIME))' $(if $(E2E_SAVE_DIR),--save-dir '$(call E2E_PATH,$(E2E_SAVE_DIR))') --case $(E2E_CASE) --launch --timeout $(E2E_TIMEOUT)
$(E2E_PYTHON) tools/e2e.py --game-dir '$(call E2E_PATH,$(GAME_DIR))' --build-dir '$(call E2E_PATH,$(E2E_BUILD))' --runtime-dir '$(call E2E_PATH,$(E2E_RUNTIME))' $(if $(E2E_SAVE_DIR),--save-dir '$(call E2E_PATH,$(E2E_SAVE_DIR))') --case $(E2E_CASE) --launch --timeout $(E2E_TIMEOUT) $(addprefix --stage ,$(E2E_STAGE))
clean:
$(DOTNET) clean VGModAPI.sln
35 changes: 35 additions & 0 deletions VGModAPI.Abstractions/Equipment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;

namespace VGModAPI;

/// <summary>Read-only tractor-module values captured for one configuration or tooltip callback.</summary>
public sealed class TractorModule
{
/// <summary>Vanilla automatic tractor beams (amountOfBeams).</summary>
public int BeamCount { get; }
/// <summary>Manual Tractor Beams in the game's UI (amountOfBonusBeams).</summary>
public int ManualBeamCount { get; }
internal TractorModule(int beamCount, int manualBeamCount)
{ BeamCount = beamCount; ManualBeamCount = manualBeamCount; }
}

/// <summary>Changes targeting capacity, not the number of physical beams or their native eligibility rules.</summary>
public sealed class TractorTargeting
{
public int AutomaticBeamLimit { get; }
public bool AllowManualBorrowing { get; }
public TractorTargeting(int automaticBeamLimit, bool allowManualBorrowing = false)
{
if (automaticBeamLimit < 0) throw new ArgumentOutOfRangeException(nameof(automaticBeamLimit));
AutomaticBeamLimit = automaticBeamLimit; AllowManualBorrowing = allowManualBorrowing;
}
}

public interface IEquipmentService : IServiceStatus
{
/// <summary>Configure player-ship tractor targeting. Return null for vanilla behavior. The first non-null
/// configuration wins in registration order. Evaluated synchronously when the module needs it, with current
/// module values. Exceptions abstain; gameplay mutation and recursive evaluation are not allowed.
/// Disposal removes the rule. Native cargo/crew/brig checks and occupied beams remain protected.</summary>
IDisposable ConfigurePlayerTractorModules(string pluginId, Func<TractorModule, TractorTargeting?> configure);
}
11 changes: 10 additions & 1 deletion VGModAPI.Abstractions/ModServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public sealed class ModServices
private readonly ICraftingJobService _craftingJobs;
private readonly ICraftingCommandService _craftingCommands;
private readonly IHudService _hud;
private readonly IEquipmentService _equipment;
private readonly ISkillTreeService _skillTrees;
private readonly ITooltipService _tooltips;
public IEquipmentService Equipment { get { CheckThread(); return _equipment; } }
public ISkillTreeService SkillTrees { get { CheckThread(); return _skillTrees; } }
public ITooltipService Tooltips { get { CheckThread(); return _tooltips; } }
private readonly IGameplayUiService _gameplayUi;
private readonly IForgeUiService _forgeUi;
private readonly IBoardingRuleService _boardingRules;
Expand Down Expand Up @@ -65,7 +71,7 @@ public sealed class ModServices

internal ModServices(ILifecycleService lifecycle, IModInformationService mods, IModSettingsService settings, ISaveDataService saveData,
IMissionService missions, ITravelService travel, IStationService station, IRecipeService recipes, IRecipeQuoteService recipeQuotes,
ICraftingJobService craftingJobs, ICraftingCommandService craftingCommands, IHudService hud, IForgeUiService forgeUi, IBoardingRuleService boardingRules, IDungeonService dungeons, IStoryService story, IBarService bars, IWorldService world, IDialogueService dialogue, IGameService game, IOwnedItemService items, IOwnedRecipeService recipeRegistration, IGameplayUiService gameplayUi)
ICraftingJobService craftingJobs, ICraftingCommandService craftingCommands, IHudService hud, IForgeUiService forgeUi, IBoardingRuleService boardingRules, IDungeonService dungeons, IStoryService story, IBarService bars, IWorldService world, IDialogueService dialogue, IGameService game, IOwnedItemService items, IOwnedRecipeService recipeRegistration, IGameplayUiService gameplayUi, IEquipmentService equipment, ISkillTreeService skillTrees, ITooltipService tooltips)
{
_lifecycle = lifecycle ?? throw new ArgumentNullException(nameof(lifecycle));
_mods = mods ?? throw new ArgumentNullException(nameof(mods));
Expand All @@ -79,6 +85,9 @@ internal ModServices(ILifecycleService lifecycle, IModInformationService mods, I
_craftingJobs = craftingJobs ?? throw new ArgumentNullException(nameof(craftingJobs));
_craftingCommands = craftingCommands ?? throw new ArgumentNullException(nameof(craftingCommands));
_hud = hud ?? throw new ArgumentNullException(nameof(hud));
_equipment = equipment ?? throw new ArgumentNullException(nameof(equipment));
_skillTrees = skillTrees ?? throw new ArgumentNullException(nameof(skillTrees));
_tooltips = tooltips ?? throw new ArgumentNullException(nameof(tooltips));
_gameplayUi = gameplayUi ?? throw new ArgumentNullException(nameof(gameplayUi));
_forgeUi = forgeUi ?? throw new ArgumentNullException(nameof(forgeUi));
_boardingRules = boardingRules ?? throw new ArgumentNullException(nameof(boardingRules));
Expand Down
25 changes: 25 additions & 0 deletions VGModAPI.Abstractions/SkillTrees.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace VGModAPI;

/// <summary>The game's commander specializations.</summary>
public enum CommanderSpecialization
{
Leadership = 1, Mining, Drones, Engineering, Industrial, Salvaging, Economy, Offense, Defense
}

/// <summary>Read-only current values for a skill tree; retained values do not follow a replacement game.</summary>
public sealed class SkillTree
{
public string Identifier { get; }
public CommanderSpecialization? Specialization { get; }
public int MasteryLevel { get; }
public int MaximumLevel { get; }
internal SkillTree(string identifier, CommanderSpecialization? specialization, int masteryLevel, int maximumLevel)
{ Identifier = identifier; Specialization = specialization; MasteryLevel = masteryLevel; MaximumLevel = maximumLevel; }
}

public interface ISkillTreeService : IServiceStatus
{
/// <summary>Read the current commander's specialization tree. Returns null without a commander,
/// when the tree does not exist, or when integration is unavailable. Main-thread only.</summary>
SkillTree? Get(CommanderSpecialization specialization);
}
98 changes: 98 additions & 0 deletions VGModAPI.Abstractions/Tooltips.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;

namespace VGModAPI;

public enum TooltipTextStyle { Normal, Bonus, Details }

/// <summary>The game's ship equipment module families, named after the native
/// Behaviour.Equipment.Module types. Other covers native modules without a dedicated kind.</summary>
public enum ShipModuleKind
{
Tractor, Mining, Salvage, ShieldGenerator, Reactor, ReactorLight, DroneBay, TorpedoBay,
Armor, CargoScoop, EngineThrusters, HangarBay, Hull, Painter, Repair, Scanner, Turret, Other
}

/// <summary>A stat line the native module just showed, captured before extensions append.</summary>
public sealed class ModuleStatLine
{
public string Label { get; }
public string Value { get; }
internal ModuleStatLine(string label, string value)
{ Label = label; Value = value; }
}

/// <summary>Read-only ship equipment module values captured for one tooltip render.</summary>
public sealed class ShipModule
{
public ShipModuleKind Kind { get; }
public string DisplayName { get; }
public int QualityLevel { get; }
/// <summary>The vanilla stat lines currently shown, in native order.</summary>
public IReadOnlyList<ModuleStatLine> StatLines { get; }
/// <summary>Beam counts, non-null only when Kind is Tractor.</summary>
public TractorModule? Tractor { get; }
internal ShipModule(ShipModuleKind kind, string displayName, int qualityLevel,
IReadOnlyList<ModuleStatLine> statLines, TractorModule? tractor)
{ Kind = kind; DisplayName = displayName; QualityLevel = qualityLevel; StatLines = statLines; Tractor = tractor; }
}

/// <summary>Read-only inventory item values captured for one item tooltip render.</summary>
public sealed class ItemInfo
{
public string Identifier { get; }
public string DisplayName { get; }
public string Description { get; }
/// <summary>Stack size shown by the source, when a concrete stack was provided.</summary>
public int Count { get; }
public int ItemLevel { get; }
internal ItemInfo(string identifier, string displayName, string description, int count, int itemLevel)
{ Identifier = identifier; DisplayName = displayName; Description = description; Count = count; ItemLevel = itemLevel; }
}

/// <summary>Append-only tooltip content. Valid only during the tooltip callback; no Unity objects are exposed.</summary>
public sealed class Tooltip
{
private readonly List<TooltipLine> _lines = new();
private readonly Action _checkThread;
private bool _closed;
internal IReadOnlyList<TooltipLine> Lines => _lines;
internal Tooltip(Action checkThread) { _checkThread = checkThread; }
internal void Close() { _closed = true; }
internal void Check()
{ _checkThread(); if (_closed) throw new InvalidOperationException("Tooltip callback has ended."); }
public TooltipLine AddLine(string text, TooltipTextStyle style = TooltipTextStyle.Normal)
{
Check(); var line = new TooltipLine(this); line.Append(text, style); _lines.Add(line); return line;
}
}

/// <summary>A line of styled text. Append adds inline text, without introducing another row.</summary>
public sealed class TooltipLine
{
private readonly Tooltip _owner;
private readonly List<(string Text, TooltipTextStyle Style)> _spans = new();
internal IReadOnlyList<(string Text, TooltipTextStyle Style)> Spans => _spans;
internal TooltipLine(Tooltip owner) { _owner = owner; }
public TooltipLine Append(string text, TooltipTextStyle style = TooltipTextStyle.Normal)
{
_owner.Check();
if (text == null) throw new ArgumentNullException(nameof(text));
if (!Enum.IsDefined(typeof(TooltipTextStyle), style)) throw new ArgumentOutOfRangeException(nameof(style));
_spans.Add((text, style)); return this;
}
}

public interface ITooltipService : IServiceStatus
{
/// <summary>Append lines to any ship equipment module's stat list, for every native module family.
/// Called while the module builds its stats; filter by Kind for your family. The native stat
/// list renders plain label text only, so TooltipTextStyle is ignored here. Changes do not
/// retroactively rebuild existing stats; native caching is preserved.</summary>
IDisposable RegisterShipModule(string pluginId, Action<ShipModule, Tooltip> describe);
/// <summary>Append lines after any inventory item's native tooltip body, using the item the
/// source is showing (inventory, shop, loot and compare sources all route through it).</summary>
IDisposable RegisterItem(string pluginId, Action<ItemInfo, Tooltip> describe);
/// <summary>Append after any skill tree's native mastery bonus lines, using live tree values.</summary>
IDisposable RegisterSkillTree(string pluginId, Action<SkillTree, Tooltip> describe);
}
74 changes: 74 additions & 0 deletions VGModAPI.Core/EquipmentService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;

namespace VGModAPI.Core;

internal sealed class EquipmentService : IEquipmentService, IDisposable
{
private readonly LifecycleHub _hub;
private readonly IServiceStatus _status;
private readonly List<Entry> _entries = new();
private bool _disposed, _evaluating;
internal EquipmentService(LifecycleHub hub) { _hub = hub; _status = hub.Services.Get("equipment"); }
public ServiceAvailability Availability => _status.Availability;
public event Action<ServiceAvailability>? AvailabilityChanged
{ add => _status.AvailabilityChanged += value; remove => _status.AvailabilityChanged -= value; }
internal void SetAvailable(bool available)
{
_hub.CheckThread(); if (_disposed) return;
if (available) _hub.SetAvailable("equipment", "Player tractor module targeting bound.");
else _hub.SetUnavailable("equipment", ServiceUnavailableReason.BindingFailed, "Equipment integration unavailable.");
}
public IDisposable ConfigurePlayerTractorModules(string pluginId, Func<TractorModule, TractorTargeting?> configure)
{
_hub.CheckThread(); if (_disposed) throw new ObjectDisposedException(nameof(EquipmentService));
var id = new RecipeId(pluginId, "tractor").ProviderId;
if (configure == null) throw new ArgumentNullException(nameof(configure));
if (_entries.Exists(e => e.Owner == id)) throw new InvalidOperationException("Duplicate tractor rule owner.");
var entry = new Entry(this, id, configure); _entries.Add(entry); return entry;
}
internal TractorTargeting? Resolve(TractorModule module)
{
_hub.CheckThread();
if (_disposed || _evaluating || !Availability.IsAvailable) return null;
_evaluating = true;
try
{
using var scope = _hub.EnterServiceDispatch();
foreach (var entry in _entries.ToArray())
{
if (entry.Configure == null) continue;
try
{
var result = entry.Configure(module);
if (_disposed || !Availability.IsAvailable) return null;
if (entry.Configure != null && result != null) return result;
}
catch (Exception error) { _hub.ReportSubscriberFailure(entry.Owner, error); }
}
return null;
}
finally { _evaluating = false; }
}
// Native automatic targeting is not reduced: configure sharing without breaking vanilla's own pool.
internal static int AutomaticCapacity(TractorModule module, TractorTargeting targeting)
=> Math.Max(module.BeamCount, Math.Min(checked(module.BeamCount + module.ManualBeamCount), targeting.AutomaticBeamLimit));
internal static bool MayBorrow(TractorModule module, TractorTargeting targeting, int busyBeams, bool manual)
=> manual ? targeting.AllowManualBorrowing : busyBeams < AutomaticCapacity(module, targeting);
public void Dispose()
{
_hub.CheckThread(); if (_disposed) return;
_disposed = true;
foreach (var entry in _entries.ToArray()) entry.Dispose();
_hub.SetUnavailable("equipment", ServiceUnavailableReason.ApiStopped, "Equipment integration stopped.");
}
private sealed class Entry : IDisposable
{
private readonly EquipmentService _service;
internal readonly string Owner;
internal Func<TractorModule, TractorTargeting?>? Configure;
internal Entry(EquipmentService service, string owner, Func<TractorModule, TractorTargeting?> configure)
{ _service = service; Owner = owner; Configure = configure; }
public void Dispose() { _service._hub.CheckThread(); Configure = null; _service._entries.Remove(this); }
}
}
36 changes: 36 additions & 0 deletions VGModAPI.Core/SkillTreeService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;

namespace VGModAPI.Core;

internal sealed class SkillTreeService : ISkillTreeService, IDisposable
{
private readonly LifecycleHub _hub;
private readonly IServiceStatus _status;
private Func<CommanderSpecialization, SkillTree?>? _read;
private bool _disposed;
internal SkillTreeService(LifecycleHub hub) { _hub = hub; _status = hub.Services.Get("skill-trees"); }
public ServiceAvailability Availability => _status.Availability;
public event Action<ServiceAvailability>? AvailabilityChanged
{ add => _status.AvailabilityChanged += value; remove => _status.AvailabilityChanged -= value; }
internal void Bind(Func<CommanderSpecialization, SkillTree?>? read)
{
_hub.CheckThread(); if (_disposed) return;
_read = read;
if (read != null) _hub.SetAvailable("skill-trees", "Commander skill trees bound.");
else _hub.SetUnavailable("skill-trees", ServiceUnavailableReason.BindingFailed, "Skill trees unavailable.");
}
public SkillTree? Get(CommanderSpecialization specialization)
{
_hub.CheckThread();
if (!Enum.IsDefined(typeof(CommanderSpecialization), specialization)) throw new ArgumentOutOfRangeException(nameof(specialization));
if (_disposed || !Availability.IsAvailable || _hub.Services.IsStopping) return null;
try { return _read?.Invoke(specialization); }
catch (Exception error) { _hub.ReportSubscriberFailure("skill-trees", error); return null; }
}
public void Dispose()
{
_hub.CheckThread(); if (_disposed) return;
_disposed = true; _read = null;
_hub.SetUnavailable("skill-trees", ServiceUnavailableReason.ApiStopped, "Skill trees stopped.");
}
}
Loading
Loading