IndustryTags: in-world business, track, and yard callouts
Live industry/track/yard labels with cached and staggered updates so the old 10-12 ms/frame cost near busy areas is gone. /s3ind dump writes a session dump.
This commit is contained in:
parent
cc552a0246
commit
48c9b97029
9 changed files with 4723 additions and 0 deletions
|
|
@ -24,6 +24,7 @@ I originally planned on releasing individual mods, but considering my workflow o
|
||||||
| Misc Tweaks | Small QoL: cancellable autoload of the most recent save from the main menu. |
|
| Misc Tweaks | Small QoL: cancellable autoload of the most recent save from the main menu. |
|
||||||
| Quick Actions | Extra outer-ring couple/air/cut actions and a consist hover wheel on the rolling-stock pie menu. |
|
| Quick Actions | Extra outer-ring couple/air/cut actions and a consist hover wheel on the rolling-stock pie menu. |
|
||||||
| Car Cards | Fanned consist dock for the selected cut, with waybill, notes, and couple/handbrake/locate actions. |
|
| Car Cards | Fanned consist dock for the selected cut, with waybill, notes, and couple/handbrake/locate actions. |
|
||||||
|
| Industry Tags | In-world business, track, and yard callouts with live industry data. |
|
||||||
|
|
||||||
All modules are disabled by default; enable them per-module from the S³ settings page. A game restart is required for enable/disable to take effect. More modules will follow; S³ is designed to grow.
|
All modules are disabled by default; enable them per-module from the S³ settings page. A game restart is required for enable/disable to take effect. More modules will follow; S³ is designed to grow.
|
||||||
|
|
||||||
|
|
@ -223,6 +224,14 @@ Optional WaypointQueue cut dividers when that mod is installed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Industry Tags
|
||||||
|
|
||||||
|
Floating in-world callouts for businesses, per-track badges, and yard codes,
|
||||||
|
read from live industry data so mod maps are included. Double-click centers
|
||||||
|
the strategy camera. Hover highlights the related tracks. Console: `/s3ind dump`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Migrating from the standalone mods
|
## Migrating from the standalone mods
|
||||||
|
|
||||||
S³ replaces the separate **Physics Optimizer** (`RailroaderPhysicsOverhaul`) and
|
S³ replaces the separate **Physics Optimizer** (`RailroaderPhysicsOverhaul`) and
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ public static class Main
|
||||||
_registry.Register(new Modules.Popout.PopoutModule());
|
_registry.Register(new Modules.Popout.PopoutModule());
|
||||||
_registry.Register(new Modules.QuickActions.QuickActionsModule());
|
_registry.Register(new Modules.QuickActions.QuickActionsModule());
|
||||||
_registry.Register(new Modules.CarCards.CarCardsModule());
|
_registry.Register(new Modules.CarCards.CarCardsModule());
|
||||||
|
_registry.Register(new Modules.IndustryTags.IndustryTagsModule());
|
||||||
|
|
||||||
_registry.EnableConfigured();
|
_registry.EnableConfigured();
|
||||||
ModConflicts.CheckAtLoad();
|
ModConflicts.CheckAtLoad();
|
||||||
|
|
|
||||||
1647
src/Modules/IndustryTags/IndustryCatalog.cs
Normal file
1647
src/Modules/IndustryTags/IndustryCatalog.cs
Normal file
File diff suppressed because it is too large
Load diff
816
src/Modules/IndustryTags/IndustryTagOverlay.cs
Normal file
816
src/Modules/IndustryTags/IndustryTagOverlay.cs
Normal file
|
|
@ -0,0 +1,816 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Helpers;
|
||||||
|
using Model;
|
||||||
|
using Model.Ops;
|
||||||
|
using Track;
|
||||||
|
using UI;
|
||||||
|
using UI.Menu;
|
||||||
|
using UI.Tags;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace S3.Modules.IndustryTags;
|
||||||
|
|
||||||
|
sealed class IndustryTagOverlay : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static bool PointerOver { get; internal set; }
|
||||||
|
|
||||||
|
readonly List<BusinessCluster> _clusters = new();
|
||||||
|
readonly List<TrackSpot> _tracks = new();
|
||||||
|
readonly Dictionary<string, IndustryTagView> _views = new();
|
||||||
|
readonly Dictionary<string, IndustryTagView> _trackViews = new();
|
||||||
|
readonly List<TrackSpot> _slideSpots = new();
|
||||||
|
readonly List<IndustryTagView> _slideViews = new();
|
||||||
|
readonly List<Rect> _slideRects = new();
|
||||||
|
readonly List<BusinessCluster> _nextClusters = new();
|
||||||
|
readonly List<TrackSpot> _nextTracks = new();
|
||||||
|
readonly HashSet<string> _seen = new();
|
||||||
|
readonly HashSet<string> _seenTracks = new();
|
||||||
|
readonly List<string> _drop = new();
|
||||||
|
float[] _slideDeltaT = System.Array.Empty<float>();
|
||||||
|
float[] _slideDeltaLift = System.Array.Empty<float>();
|
||||||
|
bool[] _slideColliding = System.Array.Empty<bool>();
|
||||||
|
bool[] _slideNear = System.Array.Empty<bool>();
|
||||||
|
float _rebuildAt;
|
||||||
|
float _trackRebuildAt;
|
||||||
|
float _refreshAt;
|
||||||
|
float _slideAt;
|
||||||
|
float _pointerAt;
|
||||||
|
float _clickAt;
|
||||||
|
bool _clustersInitialized;
|
||||||
|
bool _tracksInitialized;
|
||||||
|
bool _collisionDirty = true;
|
||||||
|
int _lastSlideCount = -1;
|
||||||
|
int _lastScreenWidth;
|
||||||
|
int _lastScreenHeight;
|
||||||
|
Vector3 _lastSlideCameraPos;
|
||||||
|
Quaternion _lastSlideCameraRot;
|
||||||
|
bool _haveSlideCameraPose;
|
||||||
|
object? _playSession;
|
||||||
|
bool _haveCatalogSettings;
|
||||||
|
bool _catalogShowTracks;
|
||||||
|
bool _catalogShowYards;
|
||||||
|
float _catalogMergeDistance;
|
||||||
|
IndustryTagView? _clickView;
|
||||||
|
string _highlightKey = "";
|
||||||
|
string? _highlightToken;
|
||||||
|
static PersistentLoader? _loader;
|
||||||
|
static GameObject? _loadingScreen;
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (!InPlay())
|
||||||
|
{
|
||||||
|
HideImmediate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object? playSession = TrainController.Shared;
|
||||||
|
if (!ReferenceEquals(_playSession, playSession))
|
||||||
|
{
|
||||||
|
ResetForPlaySession(playSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera? cam = PlayCamera();
|
||||||
|
bool want = WantVisible();
|
||||||
|
if (!want)
|
||||||
|
{
|
||||||
|
FadeAll(cam);
|
||||||
|
TickPointer(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
var s = IndustryTagsModule.Settings;
|
||||||
|
if (!_haveCatalogSettings
|
||||||
|
|| _catalogShowTracks != s.showTrackBadges
|
||||||
|
|| _catalogShowYards != s.showYardTags)
|
||||||
|
{
|
||||||
|
_haveCatalogSettings = true;
|
||||||
|
_catalogShowTracks = s.showTrackBadges;
|
||||||
|
_catalogShowYards = s.showYardTags;
|
||||||
|
_tracksInitialized = false;
|
||||||
|
_trackRebuildAt = 0f;
|
||||||
|
}
|
||||||
|
if (!Nearly(_catalogMergeDistance, s.mergeDistance))
|
||||||
|
{
|
||||||
|
_catalogMergeDistance = s.mergeDistance;
|
||||||
|
_clustersInitialized = false;
|
||||||
|
_rebuildAt = 0f;
|
||||||
|
}
|
||||||
|
bool rebuiltIndustries = false;
|
||||||
|
bool rebuiltTracks = false;
|
||||||
|
if (!_clustersInitialized || now >= _rebuildAt)
|
||||||
|
{
|
||||||
|
_rebuildAt = now + 30f;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IndustryCatalog.Rebuild(_nextClusters, s.mergeDistance);
|
||||||
|
_clusters.Clear();
|
||||||
|
_clusters.AddRange(_nextClusters);
|
||||||
|
_clustersInitialized = true;
|
||||||
|
rebuiltIndustries = true;
|
||||||
|
if (!_tracksInitialized)
|
||||||
|
_trackRebuildAt = now + 0.05f;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { S3.Core.Log.Error($"[industrytags] rebuild: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
if (!rebuiltIndustries && (!_tracksInitialized || now >= _trackRebuildAt))
|
||||||
|
{
|
||||||
|
_trackRebuildAt = now + 30f;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_nextTracks.Clear();
|
||||||
|
if (s.showTrackBadges)
|
||||||
|
IndustryCatalog.RebuildTracks(_nextTracks);
|
||||||
|
if (s.showYardTags)
|
||||||
|
IndustryCatalog.RebuildYards(_nextTracks);
|
||||||
|
_tracks.Clear();
|
||||||
|
_tracks.AddRange(_nextTracks);
|
||||||
|
_tracksInitialized = true;
|
||||||
|
rebuiltTracks = true;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { S3.Core.Log.Error($"[industrytags] tracks: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
int detailBudget = 0;
|
||||||
|
if (now >= _refreshAt)
|
||||||
|
{
|
||||||
|
_refreshAt = now + 0.04f;
|
||||||
|
detailBudget = 1;
|
||||||
|
}
|
||||||
|
int createBudget = detailBudget;
|
||||||
|
|
||||||
|
Vector3 camGame = Vector3.zero;
|
||||||
|
bool haveCam = TryCameraGame(cam, out camGame);
|
||||||
|
|
||||||
|
float maxDist = Mathf.Max(20f, s.maxDrawDistance);
|
||||||
|
float trackDist = Mathf.Max(20f, s.trackMaxDrawDistance);
|
||||||
|
bool hideIndustryNearTracks = s.showTrackBadges && s.hideIndustryWhenTracksVisible;
|
||||||
|
_seen.Clear();
|
||||||
|
|
||||||
|
foreach (BusinessCluster cluster in _clusters)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(cluster.Key)) continue;
|
||||||
|
_seen.Add(cluster.Key);
|
||||||
|
|
||||||
|
bool inRange = true;
|
||||||
|
float distMul = 1f;
|
||||||
|
if (haveCam)
|
||||||
|
{
|
||||||
|
float dist = Vector3.Distance(camGame, cluster.GameCentroid);
|
||||||
|
inRange = dist <= maxDist;
|
||||||
|
distMul = DistMul(dist, maxDist);
|
||||||
|
if (hideIndustryNearTracks && dist <= trackDist)
|
||||||
|
inRange = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool created = false;
|
||||||
|
if (!_views.TryGetValue(cluster.Key, out IndustryTagView? view) || view == null)
|
||||||
|
{
|
||||||
|
if (!inRange || createBudget <= 0) continue;
|
||||||
|
createBudget--;
|
||||||
|
var go = new GameObject("IndustryTag " + cluster.Name);
|
||||||
|
go.transform.SetParent(transform, false);
|
||||||
|
view = go.AddComponent<IndustryTagView>();
|
||||||
|
_views[cluster.Key] = view;
|
||||||
|
created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inRange)
|
||||||
|
{
|
||||||
|
view.SetDistanceMul(distMul);
|
||||||
|
view.SetWanted(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (created || rebuiltIndustries)
|
||||||
|
FillSpanIds(view, cluster.Components);
|
||||||
|
if (created || (detailBudget > 0 && now >= view.DetailRefreshAt))
|
||||||
|
{
|
||||||
|
Color color = IndustryCatalog.TagColor(cluster.Area);
|
||||||
|
if (view.Bind(cluster.Name, IndustryCatalog.BuildDetails(cluster, s), color))
|
||||||
|
_collisionDirty = true;
|
||||||
|
view.DetailRefreshAt = NextDetailRefresh(now, cluster.Key);
|
||||||
|
detailBudget--;
|
||||||
|
}
|
||||||
|
Vector3 world = cluster.GameCentroid.GameToWorld();
|
||||||
|
view.GamePos = cluster.GameCentroid;
|
||||||
|
if (view.SetWorld(world))
|
||||||
|
_collisionDirty = true;
|
||||||
|
view.SetDistanceMul(distMul);
|
||||||
|
view.SetWanted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_seenTracks.Clear();
|
||||||
|
_slideSpots.Clear();
|
||||||
|
_slideViews.Clear();
|
||||||
|
bool drawTrackLayer = s.showTrackBadges || s.showYardTags;
|
||||||
|
if (drawTrackLayer)
|
||||||
|
{
|
||||||
|
foreach (TrackSpot spot in _tracks)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(spot.Key)) continue;
|
||||||
|
bool wantThis = spot.Yard ? s.showYardTags : s.showTrackBadges;
|
||||||
|
if (!wantThis) continue;
|
||||||
|
_seenTracks.Add(spot.Key);
|
||||||
|
|
||||||
|
bool inRange = true;
|
||||||
|
float distMul = 1f;
|
||||||
|
if (haveCam)
|
||||||
|
{
|
||||||
|
float dist = Vector3.Distance(camGame, spot.GameCentroid);
|
||||||
|
inRange = dist <= trackDist;
|
||||||
|
distMul = DistMul(dist, trackDist);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool created = false;
|
||||||
|
if (!_trackViews.TryGetValue(spot.Key, out IndustryTagView? view) || view == null)
|
||||||
|
{
|
||||||
|
if (!inRange || createBudget <= 0) continue;
|
||||||
|
createBudget--;
|
||||||
|
var go = new GameObject((spot.Yard ? "YardTag " : "TrackBadge ") + spot.Label);
|
||||||
|
go.transform.SetParent(transform, false);
|
||||||
|
view = go.AddComponent<IndustryTagView>();
|
||||||
|
view.TrackBadge = true;
|
||||||
|
view.Yard = spot.Yard;
|
||||||
|
_trackViews[spot.Key] = view;
|
||||||
|
created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Yard = spot.Yard;
|
||||||
|
if (!inRange)
|
||||||
|
{
|
||||||
|
view.TrackT = 0.5f;
|
||||||
|
view.HeightLift = 0f;
|
||||||
|
view.SetDistanceMul(distMul);
|
||||||
|
view.SetWanted(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (created || rebuiltTracks)
|
||||||
|
{
|
||||||
|
view.SpanIds.Clear();
|
||||||
|
view.SpanIds.AddRange(spot.SpanIds);
|
||||||
|
view.HighlightKey = string.Join("|", view.SpanIds);
|
||||||
|
}
|
||||||
|
if (created || (detailBudget > 0 && now >= view.DetailRefreshAt))
|
||||||
|
{
|
||||||
|
Color color = IndustryCatalog.TagColor(spot.Area);
|
||||||
|
if (view.Bind(IndustryCatalog.TrackTitle(spot), IndustryCatalog.BuildTrackDetails(spot, s), color, trackBadge: true))
|
||||||
|
_collisionDirty = true;
|
||||||
|
view.DetailRefreshAt = NextDetailRefresh(now, spot.Key);
|
||||||
|
detailBudget--;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 game = spot.GameCentroid;
|
||||||
|
if (spot.PathLength >= 8f)
|
||||||
|
{
|
||||||
|
view.TrackT = IndustryCatalog.ClampPathT(spot, view.TrackT);
|
||||||
|
game = IndustryCatalog.PointOnPath(spot, view.TrackT);
|
||||||
|
}
|
||||||
|
Vector3 world = game.GameToWorld();
|
||||||
|
view.GamePos = game;
|
||||||
|
if (view.SetWorld(world))
|
||||||
|
_collisionDirty = true;
|
||||||
|
view.SetDistanceMul(distMul);
|
||||||
|
view.SetWanted(true);
|
||||||
|
_slideSpots.Add(spot);
|
||||||
|
_slideViews.Add(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SlideTrackBadges(cam, now);
|
||||||
|
|
||||||
|
TickAppearances(cam);
|
||||||
|
|
||||||
|
TickPointerBounded(cam, now);
|
||||||
|
|
||||||
|
if (rebuiltIndustries && _views.Count > _seen.Count)
|
||||||
|
{
|
||||||
|
_drop.Clear();
|
||||||
|
foreach (var kv in _views)
|
||||||
|
{
|
||||||
|
if (_seen.Contains(kv.Key)) continue;
|
||||||
|
_drop.Add(kv.Key);
|
||||||
|
if (kv.Value != null) Destroy(kv.Value.gameObject);
|
||||||
|
}
|
||||||
|
foreach (string key in _drop)
|
||||||
|
_views.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((rebuiltTracks && _trackViews.Count > _seenTracks.Count) || !drawTrackLayer)
|
||||||
|
{
|
||||||
|
_drop.Clear();
|
||||||
|
foreach (var kv in _trackViews)
|
||||||
|
{
|
||||||
|
if (drawTrackLayer && _seenTracks.Contains(kv.Key)) continue;
|
||||||
|
_drop.Add(kv.Key);
|
||||||
|
if (kv.Value != null) Destroy(kv.Value.gameObject);
|
||||||
|
}
|
||||||
|
foreach (string key in _drop)
|
||||||
|
_trackViews.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static float NextDetailRefresh(float now, string key)
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
int hash = 17;
|
||||||
|
for (int i = 0; i < key.Length; i++)
|
||||||
|
hash = hash * 31 + key[i];
|
||||||
|
float phase = (hash & 255) / 255f;
|
||||||
|
return now + 0.85f + phase * 0.25f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TickAppearances(Camera? cam)
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
int frame = Time.frameCount;
|
||||||
|
foreach (var kv in _views)
|
||||||
|
{
|
||||||
|
IndustryTagView? view = kv.Value;
|
||||||
|
if (view == null || !view.gameObject.activeSelf) continue;
|
||||||
|
view.TickAppearance(cam, ((frame + index++) & 3) == 0);
|
||||||
|
}
|
||||||
|
foreach (var kv in _trackViews)
|
||||||
|
{
|
||||||
|
IndustryTagView? view = kv.Value;
|
||||||
|
if (view == null || !view.gameObject.activeSelf) continue;
|
||||||
|
view.TickAppearance(cam, ((frame + index++) & 3) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TickPointerBounded(Camera? cam, float now)
|
||||||
|
{
|
||||||
|
bool click = Input.GetMouseButtonDown(0);
|
||||||
|
if (!click && now < _pointerAt) return;
|
||||||
|
_pointerAt = now + 0.04f;
|
||||||
|
TickPointer(cam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SlideTrackBadges(Camera? cam, float now)
|
||||||
|
{
|
||||||
|
int n = _slideSpots.Count;
|
||||||
|
if (cam == null || n == 0) return;
|
||||||
|
bool cameraChanged = !_haveSlideCameraPose
|
||||||
|
|| (cam.transform.position - _lastSlideCameraPos).sqrMagnitude > 0.0025f
|
||||||
|
|| Quaternion.Angle(cam.transform.rotation, _lastSlideCameraRot) > 0.1f
|
||||||
|
|| Screen.width != _lastScreenWidth
|
||||||
|
|| Screen.height != _lastScreenHeight;
|
||||||
|
bool listChanged = n != _lastSlideCount;
|
||||||
|
if (now < _slideAt || (!cameraChanged && !listChanged && !_collisionDirty))
|
||||||
|
return;
|
||||||
|
_slideAt = now + 0.1f;
|
||||||
|
_haveSlideCameraPose = true;
|
||||||
|
_lastSlideCameraPos = cam.transform.position;
|
||||||
|
_lastSlideCameraRot = cam.transform.rotation;
|
||||||
|
_lastScreenWidth = Screen.width;
|
||||||
|
_lastScreenHeight = Screen.height;
|
||||||
|
_lastSlideCount = n;
|
||||||
|
_collisionDirty = false;
|
||||||
|
|
||||||
|
if (n == 1)
|
||||||
|
{
|
||||||
|
EaseHome(_slideSpots[0], _slideViews[0]);
|
||||||
|
ApplySlidePositions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float collidePad = 14f;
|
||||||
|
const float clearPad = 42f;
|
||||||
|
EnsureSlideCapacity(n);
|
||||||
|
for (int round = 0; round < 3; round++)
|
||||||
|
{
|
||||||
|
if (!MeasureSlideRects(cam)) return;
|
||||||
|
System.Array.Clear(_slideDeltaT, 0, n);
|
||||||
|
System.Array.Clear(_slideDeltaLift, 0, n);
|
||||||
|
System.Array.Clear(_slideColliding, 0, n);
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
for (int j = i + 1; j < n; j++)
|
||||||
|
{
|
||||||
|
if (!RectOverlap(_slideRects[i], _slideRects[j], collidePad, out float ox, out float oy))
|
||||||
|
continue;
|
||||||
|
_slideColliding[i] = true;
|
||||||
|
_slideColliding[j] = true;
|
||||||
|
PushPair(cam, i, j, Mathf.Min(ox, oy), _slideDeltaT, _slideDeltaLift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool moved = false;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
if (!_slideColliding[i]) continue;
|
||||||
|
float nextT = IndustryCatalog.ClampPathT(_slideSpots[i], _slideViews[i].TrackT + _slideDeltaT[i]);
|
||||||
|
float nextL = Mathf.Clamp(_slideViews[i].HeightLift + _slideDeltaLift[i], 0f, 220f);
|
||||||
|
if (Mathf.Abs(nextT - _slideViews[i].TrackT) > 0.00015f
|
||||||
|
|| Mathf.Abs(nextL - _slideViews[i].HeightLift) > 0.05f)
|
||||||
|
moved = true;
|
||||||
|
_slideViews[i].TrackT = nextT;
|
||||||
|
_slideViews[i].HeightLift = nextL;
|
||||||
|
}
|
||||||
|
if (!moved) break;
|
||||||
|
ApplySlidePositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MeasureSlideRects(cam)) return;
|
||||||
|
System.Array.Clear(_slideNear, 0, n);
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
for (int j = i + 1; j < n; j++)
|
||||||
|
{
|
||||||
|
if (!RectOverlap(_slideRects[i], _slideRects[j], clearPad, out _, out _))
|
||||||
|
continue;
|
||||||
|
_slideNear[i] = true;
|
||||||
|
_slideNear[j] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
if (_slideNear[i]) continue;
|
||||||
|
EaseHome(_slideSpots[i], _slideViews[i]);
|
||||||
|
}
|
||||||
|
ApplySlidePositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureSlideCapacity(int n)
|
||||||
|
{
|
||||||
|
if (_slideDeltaT.Length >= n) return;
|
||||||
|
int size = Mathf.NextPowerOfTwo(Mathf.Max(4, n));
|
||||||
|
_slideDeltaT = new float[size];
|
||||||
|
_slideDeltaLift = new float[size];
|
||||||
|
_slideColliding = new bool[size];
|
||||||
|
_slideNear = new bool[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
void PushPair(Camera cam, int i, int j, float pen, float[] dT, float[] dL)
|
||||||
|
{
|
||||||
|
TrackSpot spotI = _slideSpots[i];
|
||||||
|
TrackSpot spotJ = _slideSpots[j];
|
||||||
|
IndustryTagView viewI = _slideViews[i];
|
||||||
|
IndustryTagView viewJ = _slideViews[j];
|
||||||
|
float tI = viewI.TrackT;
|
||||||
|
float tJ = viewJ.TrackT;
|
||||||
|
float dist0 = Vector2.Distance(PathScreen(cam, spotI, tI), PathScreen(cam, spotJ, tJ));
|
||||||
|
float step = Mathf.Clamp(PixelsToPathT(cam, spotI, tI, Mathf.Max(12f, pen * 0.4f)), 0.05f, 0.14f);
|
||||||
|
|
||||||
|
int bestSI = 0;
|
||||||
|
int bestSJ = 0;
|
||||||
|
float bestDist = dist0;
|
||||||
|
for (int s = 0; s < 4; s++)
|
||||||
|
{
|
||||||
|
int si = s == 0 || s == 2 ? 1 : -1;
|
||||||
|
int sj = s == 0 || s == 3 ? -1 : 1;
|
||||||
|
float d = Vector2.Distance(
|
||||||
|
PathScreen(cam, spotI, tI + si * step),
|
||||||
|
PathScreen(cam, spotJ, tJ + sj * step));
|
||||||
|
if (d <= bestDist + 0.75f) continue;
|
||||||
|
bestDist = d;
|
||||||
|
bestSI = si;
|
||||||
|
bestSJ = sj;
|
||||||
|
}
|
||||||
|
|
||||||
|
float gain = bestDist - dist0;
|
||||||
|
bool useHeight = gain < 16f;
|
||||||
|
float slideShare = gain < 5f ? 0f : (useHeight ? 0.18f : 1f);
|
||||||
|
if (slideShare > 0f && (bestSI != 0 || bestSJ != 0))
|
||||||
|
{
|
||||||
|
dT[i] += bestSI * PixelsToPathT(cam, spotI, tI, pen * slideShare * 0.32f);
|
||||||
|
dT[j] += bestSJ * PixelsToPathT(cam, spotJ, tJ, pen * slideShare * 0.32f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useHeight)
|
||||||
|
{
|
||||||
|
Vector3 worldI = viewI.GamePos.GameToWorld();
|
||||||
|
Vector3 worldJ = viewJ.GamePos.GameToWorld();
|
||||||
|
float distI = (cam.transform.position - worldI).sqrMagnitude;
|
||||||
|
float distJ = (cam.transform.position - worldJ).sqrMagnitude;
|
||||||
|
float lift = YOffsetForPixels(cam, distI <= distJ ? worldJ : worldI, pen * 0.42f);
|
||||||
|
if (distI <= distJ)
|
||||||
|
{
|
||||||
|
dL[j] += lift;
|
||||||
|
dL[i] -= lift * 0.4f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dL[i] += lift;
|
||||||
|
dL[j] -= lift * 0.4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dL[i] -= viewI.HeightLift * 0.45f;
|
||||||
|
dL[j] -= viewJ.HeightLift * 0.45f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vector2 PathScreen(Camera cam, TrackSpot spot, float t)
|
||||||
|
{
|
||||||
|
t = IndustryCatalog.ClampPathT(spot, t);
|
||||||
|
Vector3 s = cam.WorldToScreenPoint(IndustryCatalog.PointOnPath(spot, t).GameToWorld());
|
||||||
|
return new Vector2(s.x, s.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void EaseHome(TrackSpot spot, IndustryTagView view)
|
||||||
|
{
|
||||||
|
float k = 1f - Mathf.Exp(-5.5f * Time.unscaledDeltaTime);
|
||||||
|
float homeT = IndustryCatalog.ClampPathT(spot, 0.5f);
|
||||||
|
view.TrackT = Mathf.Lerp(view.TrackT, homeT, k);
|
||||||
|
if (Mathf.Abs(view.TrackT - homeT) < 0.003f)
|
||||||
|
view.TrackT = homeT;
|
||||||
|
view.HeightLift = Mathf.Lerp(view.HeightLift, 0f, k);
|
||||||
|
if (view.HeightLift < 0.25f)
|
||||||
|
view.HeightLift = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MeasureSlideRects(Camera cam)
|
||||||
|
{
|
||||||
|
int n = _slideViews.Count;
|
||||||
|
_slideRects.Clear();
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
if (!_slideViews[i].TryScreenRect(cam, out Rect rect))
|
||||||
|
return false;
|
||||||
|
_slideRects.Add(rect);
|
||||||
|
}
|
||||||
|
return _slideRects.Count == n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool RectOverlap(Rect a, Rect b, float pad, out float ox, out float oy)
|
||||||
|
{
|
||||||
|
ox = Mathf.Min(a.xMax + pad, b.xMax + pad) - Mathf.Max(a.xMin - pad, b.xMin - pad);
|
||||||
|
oy = Mathf.Min(a.yMax + pad, b.yMax + pad) - Mathf.Max(a.yMin - pad, b.yMin - pad);
|
||||||
|
return ox > 0f && oy > 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplySlidePositions()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _slideSpots.Count; i++)
|
||||||
|
{
|
||||||
|
Vector3 game = IndustryCatalog.PointOnPath(_slideSpots[i], _slideViews[i].TrackT);
|
||||||
|
_slideViews[i].GamePos = game;
|
||||||
|
_slideViews[i].SetWorld(game.GameToWorld());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static float YOffsetForPixels(Camera cam, Vector3 world, float pixels)
|
||||||
|
{
|
||||||
|
Vector3 a = cam.WorldToScreenPoint(world);
|
||||||
|
Vector3 b = cam.WorldToScreenPoint(world + Vector3.up);
|
||||||
|
if (a.z <= 0f || b.z <= 0f)
|
||||||
|
return Mathf.Clamp(pixels * 0.2f, 8f, 80f);
|
||||||
|
float py = Mathf.Abs(b.y - a.y);
|
||||||
|
if (py < 0.5f) py = 0.5f;
|
||||||
|
return Mathf.Clamp(pixels / py, 8f, 90f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static float PixelsToPathT(Camera cam, TrackSpot spot, float t, float pixels)
|
||||||
|
{
|
||||||
|
float sample = spot.PathLength > 1f ? Mathf.Clamp(4f / spot.PathLength, 0.015f, 0.08f) : 0.04f;
|
||||||
|
Vector3 a = IndustryCatalog.PointOnPath(spot, Mathf.Clamp01(t - sample)).GameToWorld();
|
||||||
|
Vector3 b = IndustryCatalog.PointOnPath(spot, Mathf.Clamp01(t + sample)).GameToWorld();
|
||||||
|
Vector3 sa = cam.WorldToScreenPoint(a);
|
||||||
|
Vector3 sb = cam.WorldToScreenPoint(b);
|
||||||
|
float px = Vector2.Distance(new Vector2(sa.x, sa.y), new Vector2(sb.x, sb.y));
|
||||||
|
float perT = px / (sample * 2f);
|
||||||
|
if (perT < 8f) perT = 8f;
|
||||||
|
return Mathf.Clamp(pixels / perT, 0.004f, 0.18f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FillSpanIds(IndustryTagView view, List<IndustryComponent> components)
|
||||||
|
{
|
||||||
|
view.SpanIds.Clear();
|
||||||
|
foreach (IndustryComponent ic in components)
|
||||||
|
{
|
||||||
|
if (ic?.trackSpans == null) continue;
|
||||||
|
foreach (TrackSpan span in ic.trackSpans)
|
||||||
|
{
|
||||||
|
if (span == null || string.IsNullOrEmpty(span.id)) continue;
|
||||||
|
if (!view.SpanIds.Contains(span.id))
|
||||||
|
view.SpanIds.Add(span.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.HighlightKey = string.Join("|", view.SpanIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TickPointer(Camera? cam)
|
||||||
|
{
|
||||||
|
PointerOver = false;
|
||||||
|
if (cam == null)
|
||||||
|
{
|
||||||
|
SetHighlight(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Vector3 mouse = Input.mousePosition;
|
||||||
|
IndustryTagView? best = null;
|
||||||
|
float bestD = 99999f;
|
||||||
|
foreach (IndustryTagView? view in EnumerateViews())
|
||||||
|
{
|
||||||
|
if (view == null || !view.gameObject.activeSelf) continue;
|
||||||
|
if (!view.TryScreenHit(cam, mouse, out float d)) continue;
|
||||||
|
if (d >= bestD) continue;
|
||||||
|
bestD = d;
|
||||||
|
best = view;
|
||||||
|
}
|
||||||
|
PointerOver = best != null;
|
||||||
|
SetHighlight(best);
|
||||||
|
if (best == null || !Input.GetMouseButtonDown(0)) return;
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
if (_clickView == best && now - _clickAt <= 0.4f)
|
||||||
|
{
|
||||||
|
best.JumpCamera();
|
||||||
|
_clickView = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_clickView = best;
|
||||||
|
_clickAt = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<IndustryTagView> EnumerateViews()
|
||||||
|
{
|
||||||
|
foreach (var kv in _views)
|
||||||
|
if (kv.Value != null) yield return kv.Value;
|
||||||
|
foreach (var kv in _trackViews)
|
||||||
|
if (kv.Value != null) yield return kv.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetHighlight(IndustryTagView? view)
|
||||||
|
{
|
||||||
|
string key = "";
|
||||||
|
if (view != null)
|
||||||
|
key = view.HighlightKey;
|
||||||
|
if (key == _highlightKey) return;
|
||||||
|
ClearHighlight();
|
||||||
|
_highlightKey = key;
|
||||||
|
if (key.Length == 0 || view == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ctrl = SegmentIndicatorController.Shared;
|
||||||
|
if (ctrl != null)
|
||||||
|
_highlightToken = ctrl.Add(view.SpanIds);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearHighlight()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_highlightToken))
|
||||||
|
{
|
||||||
|
try { SegmentIndicatorController.Shared?.Remove(_highlightToken); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
_highlightToken = null;
|
||||||
|
_highlightKey = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetForPlaySession(object? session)
|
||||||
|
{
|
||||||
|
ClearHighlight();
|
||||||
|
_playSession = session;
|
||||||
|
foreach (var kv in _views)
|
||||||
|
if (kv.Value != null) Destroy(kv.Value.gameObject);
|
||||||
|
foreach (var kv in _trackViews)
|
||||||
|
if (kv.Value != null) Destroy(kv.Value.gameObject);
|
||||||
|
_views.Clear();
|
||||||
|
_trackViews.Clear();
|
||||||
|
_clusters.Clear();
|
||||||
|
_tracks.Clear();
|
||||||
|
_clustersInitialized = false;
|
||||||
|
_tracksInitialized = false;
|
||||||
|
_haveCatalogSettings = false;
|
||||||
|
_haveSlideCameraPose = false;
|
||||||
|
_lastSlideCount = -1;
|
||||||
|
_collisionDirty = true;
|
||||||
|
_rebuildAt = 0f;
|
||||||
|
_trackRebuildAt = 0f;
|
||||||
|
_refreshAt = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
ClearHighlight();
|
||||||
|
foreach (var kv in _views)
|
||||||
|
if (kv.Value != null) Destroy(kv.Value.gameObject);
|
||||||
|
_views.Clear();
|
||||||
|
foreach (var kv in _trackViews)
|
||||||
|
if (kv.Value != null) Destroy(kv.Value.gameObject);
|
||||||
|
_trackViews.Clear();
|
||||||
|
_clusters.Clear();
|
||||||
|
_tracks.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool WantVisible()
|
||||||
|
{
|
||||||
|
var s = IndustryTagsModule.Settings;
|
||||||
|
if (s.alwaysOn) return true;
|
||||||
|
if (!s.followTabTags) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TagController? tags = TagController.Shared;
|
||||||
|
if (tags != null) return tags.TagsVisible;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HideImmediate()
|
||||||
|
{
|
||||||
|
ClearHighlight();
|
||||||
|
PointerOver = false;
|
||||||
|
foreach (var kv in _views)
|
||||||
|
kv.Value?.HideImmediate();
|
||||||
|
foreach (var kv in _trackViews)
|
||||||
|
kv.Value?.HideImmediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FadeAll(Camera? cam)
|
||||||
|
{
|
||||||
|
foreach (var kv in _views)
|
||||||
|
kv.Value?.SetWanted(false);
|
||||||
|
foreach (var kv in _trackViews)
|
||||||
|
kv.Value?.SetWanted(false);
|
||||||
|
foreach (var kv in _views)
|
||||||
|
if (kv.Value != null && kv.Value.gameObject.activeSelf)
|
||||||
|
kv.Value.TickAppearance(cam, true);
|
||||||
|
foreach (var kv in _trackViews)
|
||||||
|
if (kv.Value != null && kv.Value.gameObject.activeSelf)
|
||||||
|
kv.Value.TickAppearance(cam, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Camera? PlayCamera()
|
||||||
|
{
|
||||||
|
Camera? cam = Camera.main;
|
||||||
|
if (cam != null) return cam;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Camera? found = null;
|
||||||
|
if (MainCameraHelper.TryGetIfNeeded(ref found) && found != null)
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryCameraGame(Camera? cam, out Vector3 gamePos)
|
||||||
|
{
|
||||||
|
gamePos = Vector3.zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (CameraSelector.shared != null)
|
||||||
|
{
|
||||||
|
gamePos = CameraSelector.shared.CurrentCameraPosition;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
if (cam == null) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
gamePos = cam.transform.GamePosition();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static float DistMul(float dist, float maxDist)
|
||||||
|
{
|
||||||
|
if (maxDist < 1f) maxDist = 1f;
|
||||||
|
if (dist >= maxDist) return 0f;
|
||||||
|
float start = maxDist * 0.85f;
|
||||||
|
if (dist <= start) return 1f;
|
||||||
|
return Mathf.InverseLerp(maxDist, start, dist);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Nearly(float a, float b) => Mathf.Abs(a - b) <= 0.0001f;
|
||||||
|
|
||||||
|
static bool InPlay()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (TrainController.Shared == null) return false;
|
||||||
|
if (SceneDescriptor.MainMenu.IsLoaded) return false;
|
||||||
|
if (!SceneDescriptor.GameUI.IsLoaded) return false;
|
||||||
|
if (LoadingScreenVisible()) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool LoadingScreenVisible()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_loadingScreen != null)
|
||||||
|
return _loadingScreen.activeInHierarchy;
|
||||||
|
if (_loader == null)
|
||||||
|
_loader = Object.FindObjectOfType<PersistentLoader>();
|
||||||
|
if (_loader == null) return false;
|
||||||
|
_loadingScreen = Traverse.Create(_loader).Field("loadingScreen").GetValue<GameObject>();
|
||||||
|
return _loadingScreen != null && _loadingScreen.activeInHierarchy;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
1449
src/Modules/IndustryTags/IndustryTagView.cs
Normal file
1449
src/Modules/IndustryTags/IndustryTagView.cs
Normal file
File diff suppressed because it is too large
Load diff
575
src/Modules/IndustryTags/IndustryTagsDumpCommand.cs
Normal file
575
src/Modules/IndustryTags/IndustryTagsDumpCommand.cs
Normal file
|
|
@ -0,0 +1,575 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Model;
|
||||||
|
using Model.Ops;
|
||||||
|
using S3.Core;
|
||||||
|
using Track;
|
||||||
|
using UI.Console;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace S3.Modules.IndustryTags;
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(ConsoleCommandHandler))]
|
||||||
|
[HarmonyPatch("_HandleSlashCommand")]
|
||||||
|
static class IndustryTagsDumpCommandPatch
|
||||||
|
{
|
||||||
|
static bool Prefix(string[] comps, ref string __result)
|
||||||
|
{
|
||||||
|
if (comps.Length == 0 || !string.Equals(comps[0], "/s3ind", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
__result = IndustryTagsDumpCommand.Handle(comps);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class IndustryTagsDumpCommand
|
||||||
|
{
|
||||||
|
internal static string Handle(string[] comps)
|
||||||
|
{
|
||||||
|
if (comps.Length >= 2)
|
||||||
|
{
|
||||||
|
string sub = comps[1].ToLowerInvariant();
|
||||||
|
if (sub == "help") return Usage();
|
||||||
|
if (sub == "yards") return DumpYards();
|
||||||
|
if (sub != "dump") return $"Unknown subcommand '{comps[1]}'. {Usage()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
string filter = "";
|
||||||
|
if (comps.Length >= 3)
|
||||||
|
filter = string.Join(" ", comps.Skip(2)).Trim();
|
||||||
|
return Dump(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Usage() =>
|
||||||
|
"Usage: /s3ind dump [name] or /s3ind yards (writes Mods/S3/*.txt)";
|
||||||
|
|
||||||
|
static string DumpYards()
|
||||||
|
{
|
||||||
|
if (TrainController.Shared == null)
|
||||||
|
return "Not in a game.";
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"=== S3 yard dump {DateTime.Now:yyyy-MM-dd HH:mm:ss} ===");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
TrackSegment[]? all = null;
|
||||||
|
try { all = UnityEngine.Object.FindObjectsOfType<TrackSegment>(); }
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Finish(sb.AppendLine("FindObjectsOfType failed: " + e.Message), "yard-dump.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
int yardN = 0;
|
||||||
|
if (all != null)
|
||||||
|
{
|
||||||
|
sb.AppendLine("-- Style.Yard segments --");
|
||||||
|
foreach (TrackSegment seg in all.OrderBy(s => s != null ? s.id : "", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (seg == null || seg.style != TrackSegment.Style.Yard) continue;
|
||||||
|
yardN++;
|
||||||
|
float len = 0f;
|
||||||
|
try { len = seg.GetLength(); } catch { }
|
||||||
|
string goName = "";
|
||||||
|
try { goName = seg.gameObject != null ? seg.gameObject.name : ""; } catch { }
|
||||||
|
string parent = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parent = seg.transform != null && seg.transform.parent != null
|
||||||
|
? seg.transform.parent.name
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
sb.AppendLine(
|
||||||
|
$" {seg.id} name={goName} parent={parent} group={seg.groupId} " +
|
||||||
|
$"len={len:F1}m/{len * 3.28084f:F0}ft avail={seg.Available} groupOn={seg.GroupEnabled}");
|
||||||
|
}
|
||||||
|
sb.AppendLine($"({yardN} yard segments)");
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
var spots = new List<TrackSpot>();
|
||||||
|
try { IndustryCatalog.RebuildTracks(spots); }
|
||||||
|
catch (Exception e) { sb.AppendLine("RebuildTracks: " + e.Message); }
|
||||||
|
int industrySpots = spots.Count;
|
||||||
|
try { IndustryCatalog.RebuildYards(spots); }
|
||||||
|
catch (Exception e) { sb.AppendLine("RebuildYards: " + e.Message); }
|
||||||
|
|
||||||
|
int tagged = 0;
|
||||||
|
sb.AppendLine($"-- clustered yard tags (industry spots excluded={industrySpots}) --");
|
||||||
|
foreach (TrackSpot spot in spots)
|
||||||
|
{
|
||||||
|
if (!spot.Yard) continue;
|
||||||
|
tagged++;
|
||||||
|
int cars = Mathf.Max(0, Mathf.FloorToInt(spot.PathLength / 15.24f));
|
||||||
|
int ft = Mathf.Max(0, Mathf.RoundToInt(spot.PathLength * 3.28084f));
|
||||||
|
string area = "";
|
||||||
|
try { area = spot.Area != null ? spot.Area.name : ""; } catch { }
|
||||||
|
sb.AppendLine(
|
||||||
|
$" {spot.Label} {ft}ft {cars} cars area={area} pathPts={spot.Path.Count} key={spot.Key}");
|
||||||
|
}
|
||||||
|
sb.AppendLine($"({tagged} yard tags with a BY-style code)");
|
||||||
|
if (yardN > 0 && tagged == 0)
|
||||||
|
sb.AppendLine("No labels matched letter+digit codes (BY1). Check segment names above.");
|
||||||
|
|
||||||
|
return Finish(sb, "yard-dump.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Dump(string filter)
|
||||||
|
{
|
||||||
|
var ops = OpsController.Shared;
|
||||||
|
var tc = TrainController.Shared;
|
||||||
|
if (ops == null || tc == null)
|
||||||
|
return "Not in a game.";
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"=== S3 industry dump {DateTime.Now:yyyy-MM-dd HH:mm:ss} ===");
|
||||||
|
if (!string.IsNullOrEmpty(filter))
|
||||||
|
sb.AppendLine($"filter: {filter}");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
Car? selected = null;
|
||||||
|
try { selected = tc.SelectedCar; } catch { }
|
||||||
|
if (selected != null)
|
||||||
|
DumpCar(sb, ops, selected, "SELECTED CAR");
|
||||||
|
|
||||||
|
Industry[]? industries = null;
|
||||||
|
try { industries = ops.AllIndustries; } catch { }
|
||||||
|
if (industries == null || industries.Length == 0)
|
||||||
|
return Finish(sb.AppendLine("No industries."));
|
||||||
|
|
||||||
|
var componentToIndustry = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
foreach (Industry industry in industries)
|
||||||
|
{
|
||||||
|
if (industry == null) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (IndustryComponent ic in industry.Components)
|
||||||
|
{
|
||||||
|
if (ic == null || string.IsNullOrEmpty(ic.Identifier)) continue;
|
||||||
|
componentToIndustry[ic.Identifier] = industry.identifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
var inbound = new Dictionary<string, List<Car>>();
|
||||||
|
var outbound = new Dictionary<string, List<Car>>();
|
||||||
|
foreach (Car car in tc.Cars)
|
||||||
|
{
|
||||||
|
if (car == null) continue;
|
||||||
|
Waybill? wb = car.Waybill;
|
||||||
|
if (!wb.HasValue) continue;
|
||||||
|
Waybill w = wb.Value;
|
||||||
|
if (w.Completed) continue;
|
||||||
|
string? destId = TryIndustryId(componentToIndustry, w.Destination.Identifier);
|
||||||
|
string? originId = w.Origin.HasValue
|
||||||
|
? TryIndustryId(componentToIndustry, w.Origin.Value.Identifier)
|
||||||
|
: null;
|
||||||
|
if (!string.IsNullOrEmpty(destId))
|
||||||
|
Add(inbound, destId, car);
|
||||||
|
if (!string.IsNullOrEmpty(originId) && originId != destId)
|
||||||
|
Add(outbound, originId, car);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("-- summary (inbound / CarsAtPosition / span.Contains / stopped / outbound) --");
|
||||||
|
int dumped = 0;
|
||||||
|
foreach (Industry industry in industries.OrderBy(i => i != null ? i.name : "", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (industry == null || string.IsNullOrEmpty(industry.identifier)) continue;
|
||||||
|
if (!Matches(industry, filter)) continue;
|
||||||
|
|
||||||
|
List<Car> atPos = CarsAtPosition(ops, industry, stoppedOnly: false);
|
||||||
|
List<Car> stopped = CarsAtPosition(ops, industry, stoppedOnly: true);
|
||||||
|
int onSpan = 0;
|
||||||
|
inbound.TryGetValue(industry.identifier, out var inCars);
|
||||||
|
outbound.TryGetValue(industry.identifier, out var outCars);
|
||||||
|
if (inCars != null)
|
||||||
|
{
|
||||||
|
foreach (Car car in inCars)
|
||||||
|
{
|
||||||
|
if (CarOnIndustrySpans(car, industry)) onSpan++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool interesting = string.IsNullOrEmpty(filter)
|
||||||
|
|| (inCars != null && inCars.Count > 0)
|
||||||
|
|| atPos.Count > 0
|
||||||
|
|| (outCars != null && outCars.Count > 0)
|
||||||
|
|| (selected != null && CarTouches(selected, industry, componentToIndustry));
|
||||||
|
if (string.IsNullOrEmpty(filter) && !interesting)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sb.AppendLine(
|
||||||
|
$"{industry.name} id={industry.identifier} " +
|
||||||
|
$"in={inCars?.Count ?? 0} atPos={atPos.Count} span={onSpan} stopped={stopped.Count} out={outCars?.Count ?? 0}");
|
||||||
|
dumped++;
|
||||||
|
}
|
||||||
|
sb.AppendLine($"({dumped} industries listed)");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
foreach (Industry industry in industries.OrderBy(i => i != null ? i.name : "", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (industry == null) continue;
|
||||||
|
if (!Matches(industry, filter)) continue;
|
||||||
|
if (string.IsNullOrEmpty(filter))
|
||||||
|
{
|
||||||
|
inbound.TryGetValue(industry.identifier, out var inCars0);
|
||||||
|
outbound.TryGetValue(industry.identifier, out var outCars0);
|
||||||
|
if ((inCars0 == null || inCars0.Count == 0) && (outCars0 == null || outCars0.Count == 0))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpIndustry(sb, ops, industry, componentToIndustry,
|
||||||
|
inbound.TryGetValue(industry.identifier, out var inList) ? inList : null,
|
||||||
|
outbound.TryGetValue(industry.identifier, out var outList) ? outList : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Finish(sb);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DumpIndustry(
|
||||||
|
StringBuilder sb,
|
||||||
|
OpsController ops,
|
||||||
|
Industry industry,
|
||||||
|
Dictionary<string, string> componentToIndustry,
|
||||||
|
List<Car>? inbound,
|
||||||
|
List<Car>? outbound)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"== {industry.name} ({industry.identifier}) ==");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (IndustryComponent ic in industry.Components)
|
||||||
|
{
|
||||||
|
if (ic == null) continue;
|
||||||
|
DumpComponent(sb, ops, ic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" components error: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var atPos = new HashSet<Car>(CarsAtPosition(ops, industry, stoppedOnly: false));
|
||||||
|
if (inbound != null && inbound.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" inbound waybills:");
|
||||||
|
foreach (Car car in inbound)
|
||||||
|
DumpCarLine(sb, ops, industry, car, atPos, dest: true);
|
||||||
|
}
|
||||||
|
if (outbound != null && outbound.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" outbound waybills:");
|
||||||
|
foreach (Car car in outbound)
|
||||||
|
DumpCarLine(sb, ops, industry, car, atPos, dest: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Car car in atPos)
|
||||||
|
{
|
||||||
|
if (inbound != null && inbound.Contains(car)) continue;
|
||||||
|
sb.AppendLine($" extra atPos (no inbound dest): {CarLabel(car)} vel={Vel(car)} type={Safe(car.CarType)}");
|
||||||
|
DumpWaybillShort(sb, car, componentToIndustry);
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DumpComponent(StringBuilder sb, OpsController ops, IndustryComponent ic)
|
||||||
|
{
|
||||||
|
string type = ic.GetType().Name;
|
||||||
|
int spanN = ic.trackSpans != null ? ic.trackSpans.Length : 0;
|
||||||
|
string vis = "?";
|
||||||
|
try { vis = ic.IsVisible ? "vis" : "hidden"; } catch { }
|
||||||
|
string filter = "";
|
||||||
|
try { filter = ic.carTypeFilter != null ? ic.carTypeFilter.ToString() : ""; } catch { }
|
||||||
|
sb.AppendLine($" [{type}] {ic.DisplayName} id={ic.Identifier} {vis} spans={spanN} filter={filter}");
|
||||||
|
|
||||||
|
if (ic.trackSpans != null)
|
||||||
|
{
|
||||||
|
foreach (TrackSpan span in ic.trackSpans)
|
||||||
|
{
|
||||||
|
if (span == null)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" span=null");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
string segs = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = span.GetSegments();
|
||||||
|
if (list != null)
|
||||||
|
segs = string.Join(",", list.Select(s => s != null ? s.id : "?"));
|
||||||
|
}
|
||||||
|
catch { segs = "err"; }
|
||||||
|
bool valid = false;
|
||||||
|
try { valid = span.IsValid; } catch { }
|
||||||
|
sb.AppendLine($" span {span.id} valid={valid} segs=[{segs}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cars = new List<Car>();
|
||||||
|
foreach (Car c in ops.CarsAtPosition(ic))
|
||||||
|
if (c != null) cars.Add(c);
|
||||||
|
sb.AppendLine($" CarsAtPosition={cars.Count}" +
|
||||||
|
(cars.Count == 0 ? "" : " " + string.Join(", ", cars.Select(CarLabel))));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" CarsAtPosition error: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DumpCarLine(StringBuilder sb, OpsController ops, Industry industry, Car car, HashSet<Car> atPos, bool dest)
|
||||||
|
{
|
||||||
|
bool span = CarOnIndustrySpans(car, industry);
|
||||||
|
bool pos = atPos.Contains(car);
|
||||||
|
string destId = "";
|
||||||
|
string originId = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Waybill? wb = car.Waybill;
|
||||||
|
if (wb.HasValue)
|
||||||
|
{
|
||||||
|
destId = wb.Value.Destination.Identifier;
|
||||||
|
if (wb.Value.Origin.HasValue)
|
||||||
|
originId = wb.Value.Origin.Value.Identifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
sb.AppendLine(
|
||||||
|
$" {CarLabel(car)} type={Safe(car.CarType)} vel={Vel(car)} " +
|
||||||
|
$"spanContains={span} atPos={pos} dest={destId} origin={originId}");
|
||||||
|
DumpLocation(sb, car);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ops.TryGetDestinationInfo(car, out var destName, out var isAt, out _, out var destPos))
|
||||||
|
sb.AppendLine($" TryGetDestinationInfo name={destName} isAt={isAt} destId={destPos.Identifier}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" TryGetDestinationInfo error: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
OpsCarPosition? here = ops.PositionForCar(car);
|
||||||
|
sb.AppendLine(here.HasValue
|
||||||
|
? $" PositionForCar {here.Value.DisplayName}/{here.Value.Identifier}"
|
||||||
|
: " PositionForCar null");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" PositionForCar error: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DumpCar(StringBuilder sb, OpsController ops, Car car, string heading)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"-- {heading}: {CarLabel(car)} --");
|
||||||
|
sb.AppendLine($" type={Safe(car.CarType)} vel={Vel(car)}");
|
||||||
|
DumpWaybillShort(sb, car, null);
|
||||||
|
DumpLocation(sb, car);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ops.TryGetDestinationInfo(car, out var destName, out var isAt, out var destWorld, out var destPos))
|
||||||
|
sb.AppendLine($" destInfo {destName} isAt={isAt} id={destPos.Identifier} world={destWorld}");
|
||||||
|
}
|
||||||
|
catch (Exception e) { sb.AppendLine($" destInfo error: {e.Message}"); }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
OpsCarPosition? here = ops.PositionForCar(car);
|
||||||
|
sb.AppendLine(here.HasValue
|
||||||
|
? $" PositionForCar {here.Value.DisplayName}/{here.Value.Identifier}"
|
||||||
|
: " PositionForCar null");
|
||||||
|
}
|
||||||
|
catch (Exception e) { sb.AppendLine($" PositionForCar error: {e.Message}"); }
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DumpWaybillShort(StringBuilder sb, Car car, Dictionary<string, string>? map)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Waybill? wb = car.Waybill;
|
||||||
|
if (!wb.HasValue)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" waybill=none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Waybill w = wb.Value;
|
||||||
|
string destInd = map != null ? TryIndustryId(map, w.Destination.Identifier) ?? "-" : "";
|
||||||
|
string originInd = "";
|
||||||
|
if (map != null && w.Origin.HasValue)
|
||||||
|
originInd = TryIndustryId(map, w.Origin.Value.Identifier) ?? "-";
|
||||||
|
sb.AppendLine(
|
||||||
|
$" waybill completed={w.Completed} dest={w.Destination.Identifier} ({destInd}) " +
|
||||||
|
$"origin={(w.Origin.HasValue ? w.Origin.Value.Identifier : "none")} ({originInd}) tag={w.Tag}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" waybill error: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DumpLocation(StringBuilder sb, Car car)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Location opsLoc = car.OpsLocation;
|
||||||
|
Location a = car.LocationA;
|
||||||
|
Location b = car.LocationB;
|
||||||
|
sb.AppendLine($" loc ops={Loc(opsLoc)} A={Loc(a)} B={Loc(b)} world={car.transform.position}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" loc error: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Loc(Location loc)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (loc.segment == null) return "null";
|
||||||
|
return $"{loc.segment.id}@{loc.distance:F1}";
|
||||||
|
}
|
||||||
|
catch { return "?"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Car> CarsAtPosition(OpsController ops, Industry industry, bool stoppedOnly)
|
||||||
|
{
|
||||||
|
var list = new List<Car>();
|
||||||
|
var seen = new HashSet<Car>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (IndustryComponent ic in industry.Components)
|
||||||
|
{
|
||||||
|
if (ic == null || ic.trackSpans == null || ic.trackSpans.Length == 0) continue;
|
||||||
|
foreach (Car car in ops.CarsAtPosition(ic))
|
||||||
|
{
|
||||||
|
if (car == null || !seen.Add(car)) continue;
|
||||||
|
if (stoppedOnly && Mathf.Abs(car.velocity) > 0.05f) continue;
|
||||||
|
list.Add(car);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool CarOnIndustrySpans(Car car, Industry industry)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Location opsLoc = car.OpsLocation;
|
||||||
|
Location a = car.LocationA;
|
||||||
|
Location b = car.LocationB;
|
||||||
|
Vector3 world = car.transform.position;
|
||||||
|
foreach (IndustryComponent ic in industry.Components)
|
||||||
|
{
|
||||||
|
if (ic?.trackSpans == null) continue;
|
||||||
|
foreach (TrackSpan span in ic.trackSpans)
|
||||||
|
{
|
||||||
|
if (span == null) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!span.IsValid) continue;
|
||||||
|
if (span.Contains(opsLoc) || span.Contains(a) || span.Contains(b))
|
||||||
|
return true;
|
||||||
|
if (span.Contains(world, 4f))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool CarTouches(Car car, Industry industry, Dictionary<string, string> map)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Waybill? wb = car.Waybill;
|
||||||
|
if (!wb.HasValue) return false;
|
||||||
|
string? dest = TryIndustryId(map, wb.Value.Destination.Identifier);
|
||||||
|
if (dest == industry.identifier) return true;
|
||||||
|
if (wb.Value.Origin.HasValue)
|
||||||
|
{
|
||||||
|
string? origin = TryIndustryId(map, wb.Value.Origin.Value.Identifier);
|
||||||
|
if (origin == industry.identifier) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string? TryIndustryId(Dictionary<string, string> map, string? componentId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(componentId)) return null;
|
||||||
|
return map.TryGetValue(componentId, out var id) ? id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Add(Dictionary<string, List<Car>> dict, string key, Car car)
|
||||||
|
{
|
||||||
|
if (!dict.TryGetValue(key, out var list))
|
||||||
|
dict[key] = list = new List<Car>();
|
||||||
|
list.Add(car);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Matches(Industry industry, string filter)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(filter)) return true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (industry.name != null && industry.name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
return true;
|
||||||
|
if (industry.identifier != null && industry.identifier.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string CarLabel(Car car)
|
||||||
|
{
|
||||||
|
try { return $"{car.DisplayName} ({car.id})"; }
|
||||||
|
catch { return car != null ? car.id : "?"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Vel(Car car)
|
||||||
|
{
|
||||||
|
try { return car.velocity.ToString("F2"); }
|
||||||
|
catch { return "?"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Safe(string? s) => string.IsNullOrEmpty(s) ? "-" : s;
|
||||||
|
|
||||||
|
static string Finish(StringBuilder sb, string fileName = "industry-dump.txt")
|
||||||
|
{
|
||||||
|
string text = sb.ToString();
|
||||||
|
try { Log.Info("[s3ind]\n" + text); }
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
string path = "(not written)";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string dir = Main.ModEntry.Path;
|
||||||
|
path = Path.Combine(dir, fileName);
|
||||||
|
File.WriteAllText(path, text);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return text + $"\nFailed to write file: {e.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"Wrote {path}\n(also in the S3 log)";
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/Modules/IndustryTags/IndustryTagsModule.cs
Normal file
93
src/Modules/IndustryTags/IndustryTagsModule.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
using System;
|
||||||
|
using HarmonyLib;
|
||||||
|
using S3.Core;
|
||||||
|
using UI;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace S3.Modules.IndustryTags;
|
||||||
|
|
||||||
|
public sealed class IndustryTagsModule : IModule
|
||||||
|
{
|
||||||
|
private const string SettingsFile = "S3.industrytags.json";
|
||||||
|
|
||||||
|
public static IndustryTagsSettings Settings { get; private set; } = new();
|
||||||
|
|
||||||
|
private static Harmony? _harmony;
|
||||||
|
private static GameObject? _hostGo;
|
||||||
|
|
||||||
|
public IndustryTagsModule()
|
||||||
|
{
|
||||||
|
Settings = SettingsStore.Load<IndustryTagsSettings>(SettingsFile);
|
||||||
|
if (Settings.poseRev < 2)
|
||||||
|
{
|
||||||
|
Settings.heightOffset = 80f;
|
||||||
|
Settings.tagScale = 0.4f;
|
||||||
|
Settings.maxDrawDistance = 850f;
|
||||||
|
if (Settings.opacity < 0.05f) Settings.opacity = 0.9f;
|
||||||
|
Settings.poseRev = 2;
|
||||||
|
SettingsStore.Save(SettingsFile, Settings);
|
||||||
|
}
|
||||||
|
if (Settings.poseRev < 3)
|
||||||
|
{
|
||||||
|
Settings.hideIndustryWhenTracksVisible = true;
|
||||||
|
Settings.poseRev = 3;
|
||||||
|
SettingsStore.Save(SettingsFile, Settings);
|
||||||
|
}
|
||||||
|
bool saveTracks = false;
|
||||||
|
if (Settings.trackScale < 0.08f) { Settings.trackScale = 0.55f; saveTracks = true; }
|
||||||
|
if (Settings.trackHeightOffset < 1f) { Settings.trackHeightOffset = 24f; saveTracks = true; }
|
||||||
|
if (Settings.trackOpacity < 0.05f) { Settings.trackOpacity = 0.9f; saveTracks = true; }
|
||||||
|
if (Settings.trackMaxDrawDistance < 20f) { Settings.trackMaxDrawDistance = 650f; saveTracks = true; }
|
||||||
|
if (Settings.titleFontSize < 6f) { Settings.titleFontSize = 14f; saveTracks = true; }
|
||||||
|
if (Settings.trackTitleFontSize < 6f) { Settings.trackTitleFontSize = 12f; saveTracks = true; }
|
||||||
|
if (saveTracks) SettingsStore.Save(SettingsFile, Settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id => "industrytags";
|
||||||
|
public string DisplayName => "Industry Tags";
|
||||||
|
public string Description =>
|
||||||
|
"In-world callouts for businesses, industry tracks, and yard numbers. " +
|
||||||
|
"Built from live map data, so modded maps work.";
|
||||||
|
|
||||||
|
public bool Enabled
|
||||||
|
{
|
||||||
|
get => Settings.enabled;
|
||||||
|
set => Settings.enabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnEnable()
|
||||||
|
{
|
||||||
|
_harmony = new Harmony("S3.industrytags");
|
||||||
|
try { _harmony.CreateClassProcessor(typeof(IndustryTagsMouseOverUiPatch)).Patch(); }
|
||||||
|
catch (Exception e) { Log.Error($"[industrytags] patch failed: {e.Message}"); }
|
||||||
|
try { _harmony.CreateClassProcessor(typeof(IndustryTagsDumpCommandPatch)).Patch(); }
|
||||||
|
catch (Exception e) { Log.Error($"[industrytags] dump command failed: {e.Message}"); }
|
||||||
|
|
||||||
|
_hostGo = new GameObject("[S3] IndustryTagsHost");
|
||||||
|
UnityEngine.Object.DontDestroyOnLoad(_hostGo);
|
||||||
|
_hostGo.AddComponent<IndustryTagOverlay>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDisable()
|
||||||
|
{
|
||||||
|
_harmony?.UnpatchAll("S3.industrytags");
|
||||||
|
_harmony = null;
|
||||||
|
if (_hostGo != null) UnityEngine.Object.Destroy(_hostGo);
|
||||||
|
_hostGo = null;
|
||||||
|
IndustryTagOverlay.PointerOver = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSettings() => Persist();
|
||||||
|
internal static void Persist() => SettingsStore.Save(SettingsFile, Settings);
|
||||||
|
|
||||||
|
public void DrawSettings() => IndustryTagsSettingsUI.Draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(GameInput), nameof(GameInput.IsMouseOverUI))]
|
||||||
|
static class IndustryTagsMouseOverUiPatch
|
||||||
|
{
|
||||||
|
static void Postfix(ref bool __result)
|
||||||
|
{
|
||||||
|
if (IndustryTagOverlay.PointerOver) __result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Modules/IndustryTags/IndustryTagsSettings.cs
Normal file
40
src/Modules/IndustryTags/IndustryTagsSettings.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace S3.Modules.IndustryTags;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class IndustryTagsSettings
|
||||||
|
{
|
||||||
|
public bool enabled = false;
|
||||||
|
|
||||||
|
public bool followTabTags = true;
|
||||||
|
public bool alwaysOn = false;
|
||||||
|
|
||||||
|
public float mergeDistance = 250f;
|
||||||
|
public float maxDrawDistance = 850f;
|
||||||
|
public float heightOffset = 80f;
|
||||||
|
public float tagScale = 0.4f;
|
||||||
|
public float opacity = 0.9f;
|
||||||
|
public float titleFontSize = 14f;
|
||||||
|
|
||||||
|
public bool showTrackBadges = true;
|
||||||
|
public bool hideIndustryWhenTracksVisible = true;
|
||||||
|
public float trackHeightOffset = 24f;
|
||||||
|
public float trackScale = 0.55f;
|
||||||
|
public float trackOpacity = 0.9f;
|
||||||
|
public float trackTitleFontSize = 12f;
|
||||||
|
public float trackMaxDrawDistance = 650f;
|
||||||
|
|
||||||
|
public bool showYardTags = true;
|
||||||
|
public bool showYardFeet = false;
|
||||||
|
public bool showYardCarLengths = false;
|
||||||
|
|
||||||
|
// Bumped when pose defaults change so old JSON is retuned once.
|
||||||
|
public int poseRev = 3;
|
||||||
|
|
||||||
|
public bool showNeeds = true;
|
||||||
|
public bool showOutputs = true;
|
||||||
|
public bool showPerformance = true;
|
||||||
|
public bool showStallReason = true;
|
||||||
|
public bool showCarCounts = true;
|
||||||
|
}
|
||||||
93
src/Modules/IndustryTags/IndustryTagsSettingsUI.cs
Normal file
93
src/Modules/IndustryTags/IndustryTagsSettingsUI.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace S3.Modules.IndustryTags;
|
||||||
|
|
||||||
|
static class IndustryTagsSettingsUI
|
||||||
|
{
|
||||||
|
public static void Draw()
|
||||||
|
{
|
||||||
|
var s = IndustryTagsModule.Settings;
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
GUILayout.BeginVertical();
|
||||||
|
GUILayout.Label("<b>Industry Tags</b> - in-world labels for businesses, tracks, and yards");
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
GUILayout.Label(
|
||||||
|
" Groups each company's tracks and puts one callout at the midpoint.\n" +
|
||||||
|
" Reads live industry data, so modded maps work without extra setup.\n" +
|
||||||
|
" Double-click a tag to center the strategy camera on that business.\n" +
|
||||||
|
" Track badges sit over each loader/unloader; hover highlights that track.\n" +
|
||||||
|
" Yard tags are smaller codes (BY1) on Style.Yard sidings, not industry spots.\n" +
|
||||||
|
" Car counts: \u2192 still coming, loading-unloading / ready to pick up, rolling outbound \u2192.\n" +
|
||||||
|
" Console: /s3ind dump [name] /s3ind yards",
|
||||||
|
GUI.skin.label);
|
||||||
|
|
||||||
|
GUILayout.Space(10f);
|
||||||
|
GUILayout.Label("<b>Visibility</b>");
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
changed |= Toggle(ref s.followTabTags, " Show with car tags (Tab)");
|
||||||
|
changed |= Toggle(ref s.alwaysOn, " Always show (ignores Tab)");
|
||||||
|
|
||||||
|
GUILayout.Space(10f);
|
||||||
|
GUILayout.Label("<b>Contents</b>");
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
changed |= Toggle(ref s.showNeeds, " Needs (inbound cargo and storage)");
|
||||||
|
changed |= Toggle(ref s.showOutputs, " Making (outbound cargo and storage)");
|
||||||
|
changed |= Toggle(ref s.showPerformance, " Contract performance");
|
||||||
|
changed |= Toggle(ref s.showStallReason, " Stall reason (Needs steel, no contract, etc.)");
|
||||||
|
changed |= Toggle(ref s.showCarCounts, " Car counts (\u2192 inbound loading/ready outbound \u2192)");
|
||||||
|
|
||||||
|
GUILayout.Space(10f);
|
||||||
|
GUILayout.Label("<b>Placement</b>");
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
changed |= Slider("Height", ref s.heightOffset, 10f, 200f, "0");
|
||||||
|
changed |= Slider("Size", ref s.tagScale, 0.1f, 1.5f, "0.00");
|
||||||
|
changed |= Slider("Title size", ref s.titleFontSize, 8f, 28f, "0");
|
||||||
|
changed |= Slider("Opacity", ref s.opacity, 0.15f, 1f, "0.00");
|
||||||
|
changed |= Slider("Merge distance", ref s.mergeDistance, 50f, 800f, "0");
|
||||||
|
changed |= Slider("Draw distance", ref s.maxDrawDistance, 100f, 2000f, "0");
|
||||||
|
|
||||||
|
GUILayout.Space(10f);
|
||||||
|
GUILayout.Label("<b>Track badges</b>");
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
changed |= Toggle(ref s.showTrackBadges, " Show a smaller badge over each industry track");
|
||||||
|
changed |= Toggle(ref s.hideIndustryWhenTracksVisible, " Hide the business tag when you are close enough to see track badges");
|
||||||
|
changed |= Slider("Track height", ref s.trackHeightOffset, 5f, 80f, "0");
|
||||||
|
changed |= Slider("Track size", ref s.trackScale, 0.1f, 1.5f, "0.00");
|
||||||
|
changed |= Slider("Track title size", ref s.trackTitleFontSize, 8f, 28f, "0");
|
||||||
|
changed |= Slider("Track opacity", ref s.trackOpacity, 0.15f, 1f, "0.00");
|
||||||
|
changed |= Slider("Track draw distance", ref s.trackMaxDrawDistance, 80f, 2000f, "0");
|
||||||
|
|
||||||
|
GUILayout.Space(10f);
|
||||||
|
GUILayout.Label("<b>Yard tags</b>");
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
changed |= Toggle(ref s.showYardTags, " Show compact codes on yard tracks (BY1)");
|
||||||
|
changed |= Toggle(ref s.showYardFeet, " Show how many feet fit");
|
||||||
|
changed |= Toggle(ref s.showYardCarLengths, " Show how many 50 ft cars fit");
|
||||||
|
|
||||||
|
GUILayout.EndVertical();
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
IndustryTagsModule.Persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Toggle(ref bool field, string label)
|
||||||
|
{
|
||||||
|
bool next = GUILayout.Toggle(field, label);
|
||||||
|
if (next == field) return false;
|
||||||
|
field = next;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Slider(string label, ref float field, float min, float max, string fmt)
|
||||||
|
{
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
GUILayout.Label(label, GUILayout.Width(120f));
|
||||||
|
float nv = GUILayout.HorizontalSlider(field, min, max, GUILayout.Width(180f));
|
||||||
|
GUILayout.Label(field.ToString(fmt), GUILayout.Width(48f));
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
if (Mathf.Abs(nv - field) <= 0.01f) return false;
|
||||||
|
field = nv;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue