Refactors event, entity, and physics management to use thread-safe and lock-free data structures (Immutable collections, ConcurrentQueue, ConcurrentDictionary, Channel) for improved concurrency and performance. Replaces O(n) queue lookups with O(1) set/dictionary checks, ensures atomic updates for shared state, and optimizes queue draining and deferred action processing. Updates related code to use new APIs and patterns, and adds documentation for thread safety and workflow.
756 lines
36 KiB
C#
756 lines
36 KiB
C#
#nullable enable
|
|
using Barotrauma.Extensions;
|
|
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class EventManager
|
|
{
|
|
private Graph intensityGraph;
|
|
private Graph targetIntensityGraph;
|
|
private Graph monsterStrengthGraph;
|
|
private const float intensityGraphUpdateInterval = 10;
|
|
private float lastIntensityUpdate;
|
|
|
|
private Vector2 pinnedPosition = new Vector2(256, 128);
|
|
private bool isDragging;
|
|
|
|
public Event? PinnedEvent { get; set; }
|
|
|
|
private bool isGraphSelected;
|
|
|
|
public void DebugDraw(SpriteBatch spriteBatch)
|
|
{
|
|
foreach (Event ev in _activeEvents)
|
|
{
|
|
Vector2 drawPos = ev.DebugDrawPos;
|
|
drawPos.Y = -drawPos.Y;
|
|
|
|
var textOffset = new Vector2(-150, 0);
|
|
spriteBatch.DrawCircle(drawPos, 600, 6, Color.White, thickness: 20);
|
|
GUI.DrawString(spriteBatch, drawPos + textOffset, ev.ToString(), Color.White, Color.Black, 0, GUIStyle.LargeFont);
|
|
}
|
|
}
|
|
|
|
public void DebugDrawHUD(SpriteBatch spriteBatch, float y)
|
|
{
|
|
foreach (ScriptedEvent scriptedEvent in _activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast<ScriptedEvent>())
|
|
{
|
|
DrawEventTargetTags(spriteBatch, scriptedEvent);
|
|
}
|
|
|
|
float theoreticalMaxMonsterStrength = 10000;
|
|
float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * (GameMain.GameSession?.Level?.Difficulty ?? 0f) / 100;
|
|
float absoluteMonsterStrength = monsterStrength / theoreticalMaxMonsterStrength;
|
|
float relativeMonsterStrength = monsterStrength / relativeMaxMonsterStrength;
|
|
|
|
GUI.DrawString(spriteBatch, new Vector2(10, y), "EventManager", Color.White, backgroundColor: Color.Black * 0.6f, font: GUIStyle.SmallFont);
|
|
DrawString("Event cooldown", Math.Max(eventCoolDown, 0), Color.White, spacing: 20);
|
|
DrawString("Current intensity", Math.Round(currentIntensity * 100), Color.Lerp(Color.White, GUIStyle.Red, currentIntensity));
|
|
DrawString("Target intensity", Math.Round(targetIntensity * 100), Color.Lerp(Color.White, GUIStyle.Red, targetIntensity));
|
|
DrawString("Crew health", Math.Round(avgCrewHealth * 100), Color.Lerp(GUIStyle.Red, GUIStyle.Green, avgCrewHealth));
|
|
DrawString("Hull integrity", Math.Round(avgHullIntegrity * 100), Color.Lerp(GUIStyle.Red, GUIStyle.Green, avgHullIntegrity));
|
|
DrawString("Flooding amount", Math.Round(floodingAmount * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, floodingAmount));
|
|
DrawString("Fire amount", Math.Round(fireAmount * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, fireAmount));
|
|
DrawString("Enemy danger", Math.Round(enemyDanger * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, enemyDanger));
|
|
DrawString("Current monster strength (total)", Math.Round(monsterStrength), Color.Lerp(GUIStyle.Green, GUIStyle.Red, relativeMonsterStrength));
|
|
DrawString("Main events", Math.Round(CumulativeMonsterStrengthMain), Color.White);
|
|
DrawString("Ruin events", Math.Round(CumulativeMonsterStrengthRuins), Color.White);
|
|
DrawString("Wreck events", Math.Round(CumulativeMonsterStrengthWrecks), Color.White);
|
|
|
|
void DrawString(string text, double value, Color textColor, int spacing = 15)
|
|
{
|
|
y += GUI.AdjustForTextScale(spacing);
|
|
GUI.DrawString(spriteBatch, new Vector2(15, y), $"{text}: {(int)value}", textColor, backgroundColor: Color.Black * 0.6f, font: GUIStyle.SmallFont);
|
|
}
|
|
|
|
#if DEBUG
|
|
if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftAlt) &&
|
|
PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.T))
|
|
{
|
|
eventCoolDown = 1.0f;
|
|
}
|
|
#endif
|
|
|
|
if (intensityGraph == null)
|
|
{
|
|
int graphDensity = 360; // 60 min
|
|
intensityGraph = new Graph(graphDensity);
|
|
targetIntensityGraph = new Graph(graphDensity);
|
|
monsterStrengthGraph = new Graph(graphDensity);
|
|
}
|
|
|
|
if (Timing.TotalTime > lastIntensityUpdate + intensityGraphUpdateInterval)
|
|
{
|
|
intensityGraph.Update(currentIntensity);
|
|
targetIntensityGraph.Update(targetIntensity);
|
|
monsterStrengthGraph.Update(relativeMonsterStrength);
|
|
lastIntensityUpdate = (float)Timing.TotalTime;
|
|
}
|
|
|
|
Rectangle graphRect = new Rectangle(15, (int)(y + GUI.AdjustForTextScale(55)), (int)(200 * GUI.xScale), (int)(100 * GUI.yScale));
|
|
bool isGraphHovered = graphRect.Contains(PlayerInput.MousePosition);
|
|
bool leftMousePressed = PlayerInput.PrimaryMouseButtonDown() || PlayerInput.PrimaryMouseButtonHeld();
|
|
bool rightMousePressed = PlayerInput.SecondaryMouseButtonHeld() || PlayerInput.SecondaryMouseButtonDown();
|
|
if (!isGraphSelected && isGraphHovered && leftMousePressed)
|
|
{
|
|
isGraphSelected = true;
|
|
}
|
|
if (isGraphSelected && rightMousePressed)
|
|
{
|
|
isGraphSelected = false;
|
|
}
|
|
Color intensityColor = Color.Lerp(Color.White, GUIStyle.Red, currentIntensity);
|
|
if (isGraphHovered || isGraphSelected)
|
|
{
|
|
int padding = 15;
|
|
int graphHeight = Math.Min((int)(GameMain.GraphicsHeight * 0.35f), GameMain.GraphicsHeight - (graphRect.Top + 3 * padding));
|
|
graphRect.Size = new Point(GameMain.GraphicsWidth - 2 * padding, graphHeight);
|
|
intensityColor = Color.Red;
|
|
GUI.DrawRectangle(spriteBatch, graphRect, Color.Black * 0.95f, isFilled: true);
|
|
}
|
|
else
|
|
{
|
|
GUI.DrawRectangle(spriteBatch, graphRect, Color.Black * 0.6f, isFilled: true);
|
|
}
|
|
intensityGraph.Draw(spriteBatch, graphRect, maxValue: 1.0f, xOffset: 0, intensityColor, (sBatch, value, order, pos) =>
|
|
{
|
|
if (isGraphHovered || isGraphSelected)
|
|
{
|
|
Vector2 bottomPoint = new Vector2(pos.X, graphRect.Bottom);
|
|
float height = 3 * GUI.yScale;
|
|
if (order % 6 == 0)
|
|
{
|
|
height *= 3;
|
|
string text = (order / 6).ToString();
|
|
var font = GUIStyle.SmallFont;
|
|
Vector2 textSize = font.MeasureString(text);
|
|
Vector2 textPos = new Vector2(bottomPoint.X - textSize.X / 2, bottomPoint.Y + height * 1.5f);
|
|
GUI.DrawString(sBatch, textPos, text, Color.White, font: font);
|
|
}
|
|
GUI.DrawLine(sBatch, bottomPoint, bottomPoint + Vector2.UnitY * height, Color.White, width: Math.Max(GUI.Scale, 1));
|
|
DrawTimeStamps(sBatch, Color.Red, pos, order);
|
|
}
|
|
});
|
|
targetIntensityGraph.Draw(spriteBatch, graphRect, maxValue: 1.0f, xOffset: 0, intensityColor * 0.5f);
|
|
if (isGraphHovered || isGraphSelected)
|
|
{
|
|
float? maxValue = 1;
|
|
Color color = Color.White;
|
|
if (relativeMonsterStrength > 1)
|
|
{
|
|
maxValue = null;
|
|
color = Color.Yellow;
|
|
}
|
|
monsterStrengthGraph.Draw(spriteBatch, graphRect, maxValue, color: color, doForEachValue: (sBatch, value, order, pos) => DrawTimeStamps(sBatch, color, pos, order));
|
|
}
|
|
|
|
void DrawTimeStamps(SpriteBatch sBatch, Color color, Vector2 pos, int order)
|
|
{
|
|
if (isGraphHovered || isGraphSelected)
|
|
{
|
|
foreach (var timeStamp in _timeStamps)
|
|
{
|
|
int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval));
|
|
if (t == order)
|
|
{
|
|
float size = 6;
|
|
Vector2 p = new Vector2(pos.X - size / 2, pos.Y - size / 2);
|
|
ShapeExtensions.DrawPoint(sBatch, p, color, size);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
GUI.DrawLine(spriteBatch,
|
|
new Vector2(graphRect.Right, graphRect.Y + graphRect.Height * (1.0f - eventThreshold)),
|
|
new Vector2(graphRect.Right + 5, graphRect.Y + graphRect.Height * (1.0f - eventThreshold)), Color.Orange, width: 3);
|
|
|
|
int yStep = (int)(20 * GUI.yScale);
|
|
y = graphRect.Bottom + yStep;
|
|
if (isGraphHovered || isGraphSelected)
|
|
{
|
|
y += yStep;
|
|
}
|
|
int x = graphRect.X;
|
|
float adjustedYStep = GUI.AdjustForTextScale(15);
|
|
if (isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew away from sub): " + ToolBox.SecondsToReadableTime(settings.FreezeDurationWhenCrewAway - crewAwayDuration), Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
}
|
|
else if (crewAwayResetTimer > 0.0f)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew just returned to the sub): " + ToolBox.SecondsToReadableTime(crewAwayResetTimer), Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
}
|
|
else if (eventCoolDown > 0.0f)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y), "Event cooldown active: " + ToolBox.SecondsToReadableTime(eventCoolDown), Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
}
|
|
else if (currentIntensity > eventThreshold)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y),
|
|
"Intensity too high for new events: " + (int)(currentIntensity * 100) + "%/" + (int)(eventThreshold * 100) + "%", Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
}
|
|
|
|
adjustedYStep = GUI.AdjustForTextScale(12);
|
|
foreach (EventSet eventSet in _pendingEventSets)
|
|
{
|
|
if (Submarine.MainSub == null) { break; }
|
|
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y), "New event (ID " + eventSet.Identifier + ") after: ", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
|
|
if (eventSet.PerCave)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near cave", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
}
|
|
if (eventSet.PerWreck)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the wreck", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
}
|
|
if (eventSet.PerRuin)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the ruins", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
}
|
|
if (roundDuration < eventSet.MinMissionTime)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y),
|
|
" " + (int) (eventSet.MinDistanceTraveled * 100.0f) + "% travelled (current: " + (int) (distanceTraveled * 100.0f) + " %)",
|
|
((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) ? Color.Lerp(GUIStyle.Yellow, GUIStyle.Red, eventSet.MinDistanceTraveled - distanceTraveled) : GUIStyle.Green) * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
}
|
|
|
|
if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y),
|
|
" intensity between " + eventSet.MinIntensity.FormatDoubleDecimal() + " and " + eventSet.MaxIntensity.FormatDoubleDecimal(),
|
|
Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
y += adjustedYStep;
|
|
}
|
|
|
|
if (roundDuration < eventSet.MinMissionTime)
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y),
|
|
" " + (int) (eventSet.MinMissionTime - roundDuration) + " s",
|
|
Color.Lerp(GUIStyle.Yellow, GUIStyle.Red, (eventSet.MinMissionTime - roundDuration)), null, 0, GUIStyle.SmallFont);
|
|
}
|
|
|
|
y += GUI.AdjustForTextScale(15);
|
|
|
|
if (y > GameMain.GraphicsHeight * 0.9f)
|
|
{
|
|
y = graphRect.Bottom + yStep * 2;
|
|
x += 300;
|
|
}
|
|
}
|
|
|
|
GUI.DrawString(spriteBatch, new Vector2(x, y), "Current events: ", Color.White * 0.9f, null, 0, GUIStyle.SmallFont);
|
|
y += yStep;
|
|
|
|
adjustedYStep = GUI.AdjustForTextScale(18);
|
|
foreach (Event ev in _activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown()))
|
|
{
|
|
GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUIStyle.SmallFont);
|
|
|
|
Rectangle rect = new Rectangle(new Point(x + 5, (int)y), GUIStyle.SmallFont.MeasureString(ev.ToString()).ToPoint());
|
|
|
|
Rectangle outlineRect = new Rectangle(rect.Location, rect.Size);
|
|
outlineRect.Inflate(4, 4);
|
|
|
|
if (PinnedEvent == ev) { GUI.DrawRectangle(spriteBatch, outlineRect, Color.White); }
|
|
|
|
if (rect.Contains(PlayerInput.MousePosition))
|
|
{
|
|
GUI.MouseCursor = CursorState.Hand;
|
|
GUI.DrawRectangle(spriteBatch, outlineRect, Color.White);
|
|
if (ev != PinnedEvent)
|
|
{
|
|
DrawEvent(spriteBatch, ev, rect);
|
|
}
|
|
else if (rightMousePressed)
|
|
{
|
|
PinnedEvent = null;
|
|
}
|
|
if (leftMousePressed)
|
|
{
|
|
PinnedEvent = ev;
|
|
}
|
|
}
|
|
|
|
y += adjustedYStep;
|
|
if (y > GameMain.GraphicsHeight * 0.9f)
|
|
{
|
|
y = graphRect.Bottom + yStep * 2;
|
|
x += 300;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void DrawPinnedEvent(SpriteBatch spriteBatch)
|
|
{
|
|
if (PinnedEvent != null)
|
|
{
|
|
Rectangle rect = DrawEvent(spriteBatch, PinnedEvent, null);
|
|
|
|
if (rect != Rectangle.Empty)
|
|
{
|
|
if (rect.Contains(PlayerInput.MousePosition) && !isDragging)
|
|
{
|
|
GUI.MouseCursor = CursorState.Move;
|
|
if (PlayerInput.PrimaryMouseButtonDown() || PlayerInput.PrimaryMouseButtonHeld())
|
|
{
|
|
isDragging = true;
|
|
}
|
|
|
|
if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonHeld())
|
|
{
|
|
PinnedEvent = null;
|
|
isDragging = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isDragging)
|
|
{
|
|
GUI.MouseCursor = CursorState.Dragging;
|
|
pinnedPosition = PlayerInput.MousePosition - (new Vector2(rect.Width / 2.0f, -24));
|
|
if (!PlayerInput.PrimaryMouseButtonHeld())
|
|
{
|
|
isDragging = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void DrawEventTargetTags(SpriteBatch spriteBatch, ScriptedEvent scriptedEvent)
|
|
{
|
|
if (Screen.Selected is GameScreen screen)
|
|
{
|
|
Camera cam = screen.Cam;
|
|
Dictionary<Entity, List<Identifier>> tagsDictionary = new Dictionary<Entity, List<Identifier>>();
|
|
foreach ((Identifier key, List<Entity> value) in scriptedEvent.Targets)
|
|
{
|
|
foreach (Entity entity in value)
|
|
{
|
|
if (tagsDictionary.ContainsKey(entity))
|
|
{
|
|
tagsDictionary[entity].Add(key);
|
|
}
|
|
else
|
|
{
|
|
tagsDictionary.Add(entity, new List<Identifier> { key });
|
|
}
|
|
}
|
|
}
|
|
|
|
Identifier identifier = scriptedEvent.Prefab.Identifier;
|
|
|
|
foreach ((Entity entity, List<Identifier> tags) in tagsDictionary)
|
|
{
|
|
if (entity.Removed) { continue; }
|
|
|
|
string text = tags.Aggregate("Tags:\n", (current, tag) => current + $" {tag.ColorizeObject()}\n").TrimEnd('\r', '\n');
|
|
if (!identifier.IsEmpty) { text = $"Event: {identifier.ColorizeObject()}\n{text}"; }
|
|
|
|
ImmutableArray<RichTextData>? richTextData = RichTextData.GetRichTextData(text, out text);
|
|
|
|
Vector2 entityPos = cam.WorldToScreen(entity.WorldPosition);
|
|
Vector2 infoSize = GUIStyle.SmallFont.MeasureString(text);
|
|
|
|
Vector2 infoPos = entityPos + new Vector2(128 * cam.Zoom, -(128 * cam.Zoom));
|
|
infoPos.Y -= infoSize.Y / 2;
|
|
|
|
Rectangle infoRect = new Rectangle(infoPos.ToPoint(), infoSize.ToPoint());
|
|
infoRect.Inflate(4, 4);
|
|
|
|
GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled: true);
|
|
GUI.DrawRectangle(spriteBatch, infoRect, Color.White, isFilled: false);
|
|
|
|
GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextData, font: GUIStyle.SmallFont);
|
|
|
|
GUI.DrawLine(spriteBatch, entityPos, new Vector2(infoRect.Location.X, infoRect.Location.Y + infoRect.Height / 2), Color.White);
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly struct DebugLine
|
|
{
|
|
public readonly Vector2 Position;
|
|
public readonly Color Color;
|
|
|
|
public DebugLine(Vector2 position, Color color)
|
|
{
|
|
Position = position;
|
|
Color = color;
|
|
}
|
|
}
|
|
|
|
private Rectangle DrawEvent(SpriteBatch spriteBatch, Event ev, Rectangle? parentRect = null)
|
|
{
|
|
return ev switch
|
|
{
|
|
ScriptedEvent scriptedEvent => DrawScriptedEvent(spriteBatch, scriptedEvent, parentRect),
|
|
ArtifactEvent artifactEvent => DrawArtifactEvent(spriteBatch, artifactEvent, parentRect),
|
|
MonsterEvent monsterEvent => DrawMonsterEvent(spriteBatch, monsterEvent, parentRect),
|
|
_ => Rectangle.Empty
|
|
};
|
|
}
|
|
|
|
private Rectangle DrawScriptedEvent(SpriteBatch spriteBatch, ScriptedEvent scriptedEvent, Rectangle? parentRect = null)
|
|
{
|
|
List<DebugLine> positions = new List<DebugLine>();
|
|
|
|
string text = scriptedEvent.GetDebugInfo();
|
|
if (scriptedEvent.Targets != null)
|
|
{
|
|
foreach ((_, List<Entity> entities) in scriptedEvent.Targets)
|
|
{
|
|
if (entities == null || !entities.Any()) { continue; }
|
|
|
|
foreach (var entity in entities)
|
|
{
|
|
positions.Add(new DebugLine(entity.WorldPosition, Color.White));
|
|
}
|
|
}
|
|
}
|
|
|
|
return DrawInfoRectangle(spriteBatch, scriptedEvent, text, parentRect, positions);
|
|
}
|
|
|
|
private readonly List<DebugLine> debugPositions = new List<DebugLine>();
|
|
|
|
private Rectangle DrawArtifactEvent(SpriteBatch spriteBatch, ArtifactEvent artifactEvent, Rectangle? parentRect = null)
|
|
{
|
|
debugPositions.Clear();
|
|
|
|
string text = artifactEvent.GetDebugInfo();
|
|
|
|
if (artifactEvent.Item != null && !artifactEvent.Item.Removed)
|
|
{
|
|
Vector2 pos = artifactEvent.Item.WorldPosition;
|
|
debugPositions.Add(new DebugLine(pos, Color.White));
|
|
}
|
|
|
|
return DrawInfoRectangle(spriteBatch, artifactEvent, text, parentRect, debugPositions);
|
|
}
|
|
|
|
private Rectangle DrawMonsterEvent(SpriteBatch spriteBatch, MonsterEvent monsterEvent, Rectangle? parentRect = null)
|
|
{
|
|
debugPositions.Clear();
|
|
|
|
string text = monsterEvent.GetDebugInfo();
|
|
|
|
if (monsterEvent.SpawnPos != null && Submarine.MainSub != null)
|
|
{
|
|
Vector2 pos = monsterEvent.SpawnPos.Value;
|
|
text += $"Distance from submarine: {Vector2.Distance(pos, Submarine.MainSub.WorldPosition).ColorizeObject()}\n";
|
|
debugPositions.Add(new DebugLine(pos, Color.White));
|
|
}
|
|
|
|
if (monsterEvent.Monsters != null)
|
|
{
|
|
text += !monsterEvent.Monsters.Any() ? $"Monsters: {"None".ColorizeObject()}" : "Monsters:\n";
|
|
|
|
foreach (Character monster in monsterEvent.Monsters)
|
|
{
|
|
text += $" {monster.ColorizeObject()} -> (Dead: {monster.IsDead.ColorizeObject()}, Health: {monster.HealthPercentage.ColorizeObject()}%, AIState: {(monster.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle ).ColorizeObject()})\n";
|
|
if (monster.Removed) { continue; }
|
|
debugPositions.Add(new DebugLine(monster.WorldPosition, Color.Red));
|
|
}
|
|
}
|
|
return DrawInfoRectangle(spriteBatch, monsterEvent, text, parentRect, debugPositions);
|
|
}
|
|
|
|
private Rectangle DrawInfoRectangle(SpriteBatch spriteBatch, Event @event, string text, Rectangle? parentRect = null, List<DebugLine>? drawPoints = null)
|
|
{
|
|
text = text.TrimEnd('\r', '\n');
|
|
|
|
Identifier identifier = @event.Prefab.Identifier;
|
|
if (!identifier.IsEmpty)
|
|
{
|
|
text = $"Identifier: {identifier.ColorizeObject()}\n{text}";
|
|
}
|
|
|
|
ImmutableArray<RichTextData>? richTextData = RichTextData.GetRichTextData(text, out text);
|
|
|
|
Vector2 size = GUIStyle.SmallFont.MeasureString(text);
|
|
Vector2 pos = pinnedPosition;
|
|
Rectangle infoRect;
|
|
Rectangle? infoBarRect = null;
|
|
|
|
if (parentRect != null)
|
|
{
|
|
Rectangle rect = parentRect.Value;
|
|
pos = new Vector2(350, GameMain.GraphicsHeight / 2.0f - size.Y / 2);
|
|
infoRect = new Rectangle(pos.ToPoint(), size.ToPoint());
|
|
infoRect.Inflate(8, 8);
|
|
|
|
GUI.DrawLine(spriteBatch, new Vector2(rect.Right, rect.Y + rect.Height / 2), new Vector2(infoRect.X, infoRect.Y + infoRect.Height / 2), Color.White);
|
|
}
|
|
else
|
|
{
|
|
infoRect = new Rectangle(pos.ToPoint(), size.ToPoint());
|
|
infoRect.Inflate(8, 8);
|
|
|
|
Rectangle barRect = new Rectangle(infoRect.Left, infoRect.Top - 32, infoRect.Width, 32);
|
|
const string titleHeader = "Pinned event";
|
|
|
|
GUI.DrawRectangle(spriteBatch, barRect, Color.DarkGray * 0.8f, isFilled: true);
|
|
GUI.DrawString(spriteBatch, barRect.Location.ToVector2() + barRect.Size.ToVector2() / 2 - GUIStyle.SubHeadingFont.MeasureString(titleHeader) / 2, titleHeader, Color.White);
|
|
GUI.DrawRectangle(spriteBatch, barRect, Color.White);
|
|
infoBarRect = barRect;
|
|
}
|
|
|
|
if (drawPoints != null && drawPoints.Any() && Screen.Selected?.Cam != null)
|
|
{
|
|
foreach (DebugLine line in drawPoints)
|
|
{
|
|
if (line.Position != Vector2.Zero)
|
|
{
|
|
float xPos = infoRect.Right;
|
|
|
|
if (parentRect == null && pinnedPosition.X + infoRect.Width / 2.0f > GameMain.GraphicsWidth / 2.0f)
|
|
{
|
|
xPos = infoRect.Left;
|
|
}
|
|
|
|
GUI.DrawLine(spriteBatch, new Vector2(xPos, infoRect.Top + infoRect.Height / 2), Screen.Selected.Cam.WorldToScreen(line.Position), line.Color);
|
|
}
|
|
}
|
|
}
|
|
|
|
GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled: true);
|
|
GUI.DrawRectangle(spriteBatch, infoRect, Color.White);
|
|
|
|
if (richTextData.HasValue && richTextData.Value.Length > 0)
|
|
{
|
|
GUI.DrawStringWithColors(spriteBatch, pos, text, Color.White, richTextData.Value, null, 0, GUIStyle.SmallFont);
|
|
}
|
|
else
|
|
{
|
|
GUI.DrawString(spriteBatch, pos, text, Color.White, null, 0, GUIStyle.SmallFont);
|
|
}
|
|
return infoBarRect ?? infoRect;
|
|
}
|
|
|
|
public void ClientRead(IReadMessage msg)
|
|
{
|
|
if (GameMain.GameSession.IsRunning && !GameMain.Instance.LoadingScreenOpen)
|
|
{
|
|
ClientApplyNetworkMessage(msg);
|
|
}
|
|
else
|
|
{
|
|
//if the game session is not currently running (round still loading),
|
|
//we need to wait because the entities the status effect / conversation / etc targets may not exist yet
|
|
CoroutineManager.StartCoroutine(ApplyNetworkMessageWhenRoundLoaded(msg));
|
|
}
|
|
}
|
|
|
|
public IEnumerable<CoroutineStatus> ApplyNetworkMessageWhenRoundLoaded(IReadMessage msg)
|
|
{
|
|
while (GameMain.GameSession is { IsRunning: false } || GameMain.Instance.LoadingScreenOpen)
|
|
{
|
|
yield return new WaitForSeconds(1.0f);
|
|
}
|
|
if (GameMain.GameSession != null && GameMain.Client != null)
|
|
{
|
|
ClientApplyNetworkMessage(msg);
|
|
}
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
public void ClientApplyNetworkMessage(IReadMessage msg)
|
|
{
|
|
NetworkEventType eventType = (NetworkEventType)msg.ReadByte();
|
|
switch (eventType)
|
|
{
|
|
case NetworkEventType.STATUSEFFECT:
|
|
Identifier eventIdentifier = msg.ReadIdentifier();
|
|
UInt16 actionIndex = msg.ReadUInt16();
|
|
UInt16 targetCount = msg.ReadUInt16();
|
|
List<Entity> targets = new List<Entity>();
|
|
for (int i = 0; i < targetCount; i++)
|
|
{
|
|
UInt16 targetID = msg.ReadUInt16();
|
|
Entity target = Entity.FindEntityByID(targetID);
|
|
if (target != null) { targets.Add(target); }
|
|
}
|
|
|
|
var eventPrefab = EventSet.GetEventPrefab(eventIdentifier);
|
|
if (eventPrefab == null) { return; }
|
|
int j = 0;
|
|
foreach (var element in eventPrefab.ConfigElement.Descendants())
|
|
{
|
|
if (j != actionIndex)
|
|
{
|
|
j++;
|
|
continue;
|
|
}
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; }
|
|
StatusEffect effect = StatusEffect.Load(subElement, $"EventManager.ClientRead ({eventIdentifier})");
|
|
foreach (Entity target in targets)
|
|
{
|
|
if (target is Item item)
|
|
{
|
|
effect.Apply(effect.type, 1.0f, item, item.AllPropertyObjects);
|
|
}
|
|
else
|
|
{
|
|
effect.Apply(effect.type, 1.0f, target, target as ISerializableEntity);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case NetworkEventType.CONVERSATION:
|
|
{
|
|
UInt16 identifier = msg.ReadUInt16();
|
|
string eventSprite = msg.ReadString();
|
|
byte dialogType = msg.ReadByte();
|
|
bool continueConversation = msg.ReadBoolean();
|
|
UInt16 speakerId = msg.ReadUInt16();
|
|
string text = msg.ReadString();
|
|
bool fadeToBlack = msg.ReadBoolean();
|
|
byte optionCount = msg.ReadByte();
|
|
List<string> options = new List<string>();
|
|
for (int i = 0; i < optionCount; i++)
|
|
{
|
|
options.Add(msg.ReadString());
|
|
}
|
|
|
|
byte endCount = msg.ReadByte();
|
|
int[] endings = new int[endCount];
|
|
for (int i = 0; i < endCount; i++)
|
|
{
|
|
endings[i] = msg.ReadByte();
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(text) && optionCount == 0)
|
|
{
|
|
GUIMessageBox.MessageBoxes.ForEachMod(mb =>
|
|
{
|
|
if (mb.UserData is Pair<string, UInt16> pair && pair.First == "ConversationAction" && pair.Second == identifier)
|
|
{
|
|
(mb as GUIMessageBox)?.Close();
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation);
|
|
}
|
|
if (Entity.FindEntityByID(speakerId) is Character speaker)
|
|
{
|
|
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
|
|
speaker.SetCustomInteract(null, null);
|
|
}
|
|
break;
|
|
}
|
|
case NetworkEventType.CONVERSATION_SELECTED_OPTION:
|
|
{
|
|
UInt16 identifier = msg.ReadUInt16();
|
|
int selectedOption = msg.ReadByte() - 1;
|
|
ConversationAction.SelectOption(identifier, selectedOption);
|
|
break;
|
|
}
|
|
case NetworkEventType.MISSION:
|
|
Identifier missionIdentifier = msg.ReadIdentifier();
|
|
int locationIndex = msg.ReadInt32();
|
|
int destinationIndex = msg.ReadInt32();
|
|
string missionName = msg.ReadString();
|
|
if (Screen.Selected != GameMain.NetLobbyScreen)
|
|
{
|
|
MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier);
|
|
if (prefab != null)
|
|
{
|
|
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName),
|
|
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
|
|
{
|
|
IconColor = prefab.IconColor
|
|
};
|
|
if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count)
|
|
{
|
|
Location location = map.Locations[locationIndex];
|
|
map.Discover(location, checkTalents: false);
|
|
|
|
LocationConnection? connection = null;
|
|
if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count)
|
|
{
|
|
Location destination = map.Locations[destinationIndex];
|
|
connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination));
|
|
}
|
|
if (connection != null)
|
|
{
|
|
location.UnlockMission(prefab, connection);
|
|
}
|
|
else
|
|
{
|
|
location.UnlockMission(prefab);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case NetworkEventType.UNLOCKPATH:
|
|
UInt16 connectionIndex = msg.ReadUInt16();
|
|
if (GameMain.GameSession?.Map?.Connections != null)
|
|
{
|
|
if (connectionIndex >= GameMain.GameSession.Map.Connections.Count)
|
|
{
|
|
DebugConsole.ThrowError($"Failed to unlock a path on the campaign map. Connection index out of bounds (index: {connectionIndex}, number of connections: {GameMain.GameSession.Map.Connections.Count})");
|
|
}
|
|
else
|
|
{
|
|
GameMain.GameSession.Map.Connections[connectionIndex].Locked = false;
|
|
new GUIMessageBox(string.Empty, TextManager.Get("pathunlockedgeneric"),
|
|
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128));
|
|
}
|
|
}
|
|
break;
|
|
case NetworkEventType.EVENTLOG:
|
|
ClientReadEventLog(GameMain.Client, msg);
|
|
break;
|
|
case NetworkEventType.EVENTOBJECTIVE:
|
|
ClientReadEventObjective(GameMain.Client, msg);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ClientReadEventLog(GameClient client, IReadMessage msg)
|
|
{
|
|
NetEventLogEntry entry = INetSerializableStruct.Read<NetEventLogEntry>(msg);
|
|
EventLog.AddEntry(entry.EventPrefabId, entry.LogEntryId, entry.Text.Replace("\\n", "\n"));
|
|
}
|
|
private static void ClientReadEventObjective(GameClient client, IReadMessage msg)
|
|
{
|
|
NetEventObjective entry = INetSerializableStruct.Read<NetEventObjective>(msg);
|
|
EventObjectiveAction.Trigger(
|
|
entry.Type,
|
|
entry.Identifier,
|
|
entry.ObjectiveTag,
|
|
entry.ParentObjectiveId,
|
|
entry.TextTag,
|
|
entry.CanBeCompleted);
|
|
}
|
|
}
|
|
}
|