blizzless-diiis/src/DiIiS-NA/D3-GameServer/GSSystem/MapSystem/World.cs

1550 lines
48 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Drawing;
using System.Linq;
using DiIiS_NA.Core.Extensions;
using DiIiS_NA.Core.Helpers.Hash;
using DiIiS_NA.Core.Helpers.Math;
using DiIiS_NA.Core.Logging;
using DiIiS_NA.Core.MPQ;
using DiIiS_NA.Core.MPQ.FileFormats;
using DiIiS_NA.D3_GameServer;
using DiIiS_NA.D3_GameServer.Core.Types.SNO;
using DiIiS_NA.GameServer.Core.Types.Math;
using DiIiS_NA.GameServer.Core.Types.QuadTrees;
using DiIiS_NA.GameServer.Core.Types.SNO;
using DiIiS_NA.GameServer.Core.Types.TagMap;
using DiIiS_NA.GameServer.GSSystem.ActorSystem;
using DiIiS_NA.GameServer.GSSystem.ActorSystem.Implementations;
using DiIiS_NA.GameServer.GSSystem.ActorSystem.Movement;
using DiIiS_NA.GameServer.GSSystem.GameSystem;
using DiIiS_NA.GameServer.GSSystem.ItemsSystem;
using DiIiS_NA.GameServer.GSSystem.ObjectsSystem;
using DiIiS_NA.GameServer.GSSystem.PlayerSystem;
using DiIiS_NA.GameServer.GSSystem.PowerSystem;
using DiIiS_NA.GameServer.MessageSystem;
using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.ACD;
using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Animation;
using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Misc;
using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.World;
using DiIiS_NA.GameServer.MessageSystem.Message.Fields;
using DiIiS_NA.LoginServer.Toons;
using Actor = DiIiS_NA.GameServer.GSSystem.ActorSystem.Actor;
using Circle = DiIiS_NA.GameServer.Core.Types.Misc.Circle;
using Environment = DiIiS_NA.Core.MPQ.FileFormats.Environment;
using Monster = DiIiS_NA.GameServer.GSSystem.ActorSystem.Monster;
using ResolvedPortalDestination = DiIiS_NA.GameServer.MessageSystem.Message.Fields.ResolvedPortalDestination;
namespace DiIiS_NA.GameServer.GSSystem.MapSystem
{
public sealed class World : DynamicObject, IRevealable, IUpdateable
{
static readonly Logger Logger = LogManager.CreateLogger();
public readonly Dictionary<World, List<Item>> DbItems = new(); //we need this list to delete item_instances from items which have no owner anymore.
public readonly Dictionary<ulong, Item> CachedItems = new();
public int LastCEId = 3000;
/// <summary>
/// Game that the world belongs to.
/// </summary>
public Game Game { get; private set; }
public bool DRLGEmuActive = false;
/// <summary>
/// SNOHandle for the world.
/// </summary>
public SNOHandle WorldSNO { get; private set; }
public WorldSno SNO => (WorldSno)WorldSNO.Id;
/// <summary>
/// QuadTree that contains scenes & actors.
/// </summary>
private QuadTree _quadTree;
public static QuadTree _PvPQuadTree = new(new Size(60, 60), 0);
public QuadTree QuadTree
{
get => (IsPvP ? _PvPQuadTree : _quadTree);
set { }
}
/// <summary>
/// WorldData loaded from MPQs/DB
/// </summary>
public DiIiS_NA.Core.MPQ.FileFormats.World worldData = new();
/// <summary>
/// Destination for portals(on Exit and DungeonStone)
/// </summary>
public ResolvedPortalDestination NextLocation;
/// <summary>
/// Destination for portals(on Entrance)
/// </summary>
public ResolvedPortalDestination PrevLocation;
/// <summary>
/// List of scenes contained in the world.
/// </summary>
private readonly ConcurrentDictionary<uint, Scene> _scenes;
private static readonly ConcurrentDictionary<uint, Scene> _PvPscenes = new();
public ConcurrentDictionary<uint, Scene> Scenes
{
get => (IsPvP ? _PvPscenes : _scenes);
set { }
}
/// <summary>
/// List of actors contained in the world.
/// </summary>
private readonly ConcurrentDictionary<uint, Actor> _actors;
public static readonly ConcurrentDictionary<uint, Actor> _PvPActors = new();
public ConcurrentDictionary<uint, Actor> Actors
{
get => (IsPvP ? _PvPActors : _actors);
set { }
}
public Dictionary<int, WorldSno> PortalOverrides = new();
/// <summary>
/// List of players contained in the world.
/// </summary>
private readonly ConcurrentDictionary<uint, Player> _players;
public static readonly ConcurrentDictionary<uint, Player> _PvPPlayers = new();
public ConcurrentDictionary<uint, Player> Players => (IsPvP ? _PvPPlayers : _players);
/// <summary>
/// Returns true if the world has players in.
/// </summary>
public bool HasPlayersIn => Players.Count > 0;
/// <summary>
/// Returns a new dynamicId for scenes.
/// </summary>
public uint NewSceneID => IsPvP ? NewPvPSceneID : Game.NewSceneId;
public bool IsPvP => SNO == WorldSno.pvp_duel_small_multi; //PvP_Duel_Small
public static bool PvPMapLoaded = false;
public Scene GetSceneBySnoId(int SnoID)
{
Scene scene = null;
foreach (var _scene in _scenes.Values)
{
if (_scene.SceneSNO.Id == SnoID)
scene = _scene;
}
return scene;
}
// Environment
public Environment Environment => ((DiIiS_NA.Core.MPQ.FileFormats.World)MPQStorage.Data.Assets[SNOGroup.Worlds][WorldSNO.Id].Data).Environment;
private static uint _lastPvPObjectID = 10001;
private static readonly object _obj = new();
public static uint NewActorPvPID
{
get
{
lock (_obj)
{
_lastPvPObjectID++;
return _lastPvPObjectID;
}
}
}
private static uint _lastPvPSceneID = 0x06000000;
public static uint NewPvPSceneID
{
get
{
lock (_obj)
{
_lastPvPSceneID++;
return _lastPvPSceneID;
}
}
}
/// <summary>
/// Returns list of available starting points.
/// </summary>
public List<StartingPoint> StartingPoints
{
get { return Actors.Values.OfType<StartingPoint>().Select(actor => actor).ToList(); }
}
public List<Portal> Portals => Actors.Values.OfType<Portal>().Select(actor => actor).ToList();
public List<Monster> Monsters
{
get { return Actors.Values.OfType<Monster>().Select(actor => actor).ToList(); }
}
private PowerManager _powerManager;
public static PowerManager _PvPPowerManager = new();
public PowerManager PowerManager => IsPvP ? _PvPPowerManager : _powerManager;
private BuffManager _buffManager;
public static BuffManager _PvPBuffManager = new();
public BuffManager BuffManager => IsPvP ? _PvPBuffManager : _buffManager;
/// <summary>
/// Creates a new world for the given game with given snoId.
/// </summary>
/// <param name="game">The parent game.</param>
/// <param name="sno">The sno for the world.</param>
public World(Game game, WorldSno sno)
: base(sno == WorldSno.pvp_duel_small_multi ? 99999 : game.NewWorldId)
{
WorldSNO = new SNOHandle(SNOGroup.Worlds, (int)sno);
Game = game;
_scenes = new ConcurrentDictionary<uint, Scene>();
_actors = new ConcurrentDictionary<uint, Actor>();
_players = new ConcurrentDictionary<uint, Player>();
_quadTree = new QuadTree(new Size(60, 60), 0);
NextLocation = PrevLocation = new ResolvedPortalDestination
{
WorldSNO = (int)WorldSno.__NONE,
DestLevelAreaSNO = -1,
StartingPointActorTag = -1
};
_powerManager = new PowerManager();
_buffManager = new BuffManager();
Game.AddWorld(this);
//this.Game.StartTracking(this); // start tracking the dynamicId for the world.
if (SNO == WorldSno.x1_bog_01) //Blood Marsh
{
var worlds = new List<WorldSno> { WorldSno.x1_catacombs_level01, WorldSno.x1_catacombs_fakeentrance_02, WorldSno.x1_catacombs_fakeentrance_03, WorldSno.x1_catacombs_fakeentrance_04 };
var scenes = new List<int> { 265624, 265655, 265678, 265693 };
foreach (var scene in scenes)
{
var wld = worlds.PickRandom();
PortalOverrides.Add(scene, wld);
worlds.Remove(wld);
}
}
}
#region update & tick logic
/// <summary>
/// Retrieve all portals located within a specified <param name="radius"/> of the given <param name="actor"/>.
/// </summary>
/// <param name="actor">The actor located near the portals</param>
/// <param name="radius">The radius of the portals to be returned is to be specified.</param>
/// <returns>Order all existing portals in the world by ascending distance from a specified <param name="actor"></param>.</returns>
/// <exception cref="ArgumentNullException">If <param name="actor"></param> is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">If <param name="radius"></param> is not null but lesser than 0.</exception>
public ImmutableArray<Portal> GetPortals(Actor actor, float? radius = null)
{
if (actor == null)
throw new ArgumentNullException(nameof(actor));
if (radius <= 0)
throw new ArgumentOutOfRangeException(nameof(radius), "Radius must be greater than zero.");
if (radius is { } r)
Logger.MethodTrace(
$"All portals near $[underline]${actor.SNO} ({actor.GetType().Name})$[/]$ within $[underline]${r}$[/]$ radius");
else
Logger.MethodTrace($"All portals near $[underline]${actor.SNO} ({actor.GetType().Name})$[/]$");
return Portals
.Where(portal =>
{
if (radius is not { } r) return true;
var position = actor.Position;
var distance = portal.Position.DistanceSquared(ref position);
return distance <= radius.Value;
})
.OrderBy(s =>
{
var position = actor.Position;
return s.Position.DistanceSquared(ref position);
})
.ToImmutableArray();
}
public void Update(int tickCounter)
{
foreach (var player in Players.Values)
{
player.InGameClient.SendTick(); // if there's available messages to send, will handle ticking and flush the outgoing buffer.
}
var actorsToUpdate = new List<IUpdateable>(); // list of actor to update.
foreach (var player in Players.Values) // get players in the world.
{
foreach (var actor in player.GetActorsInRange().OfType<IUpdateable>()) // get IUpdateable actors in range.
{
if (actorsToUpdate.Contains(actor)) // don't let a single actor in range of more than players to get updated more thance per tick /raist.
continue;
actorsToUpdate.Add(actor);
}
}
foreach (var minion in Actors.Values.OfType<Minion>())
{
if (actorsToUpdate.Contains(minion))
continue;
actorsToUpdate.Add(minion);
}
foreach (var actor in actorsToUpdate) // trigger the updates.
{
actor.Update(tickCounter);
}
BuffManager.Update();
PowerManager.Update();
if (tickCounter % 6 == 0 && _flippyTimers.Any())
{
UpdateFlippy(tickCounter);
}
}
#endregion
#region message broadcasting
/// <summary>
/// Broadcasts a message to all players in the world.
/// </summary>
/// <param name="action">The action that will be invoked to all players</param>
/// <exception cref="Exception">If there was an error to broadcast to player.</exception>
public void BroadcastOperation(Action<Player> action)
{
foreach (var player in Players.Values)
{
if (player == null) continue;
try
{
action(player);
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting to player " + player.Name, ex);
}
}
}
/// <summary>
/// Broadcasts a message to all players in the world where the <param name="predicate"></param> is true.
/// </summary>
/// <param name="predicate">Players matching criteria</param>
/// <param name="action">The action that will be invoked to all players</param>
/// <exception cref="Exception">If there was an error to broadcast to player</exception>
public void BroadcastOperation(Func<Player, bool> predicate, Action<Player> action)
{
foreach (var player in Players.Values.Where(predicate))
{
if (player == null) continue;
try
{
action(player);
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting to player " + player.Name, ex);
}
}
}
// NOTE: Scenes are actually laid out in cells with Subscenes filling in certain areas under a Scene.
// We can use this design feature to track Actors' current scene and send updates to it and neighboring
// scenes instead of distance checking for broadcasting messages. / komiga
// I'll be soon adding that feature /raist.
/// <summary>
/// Broadcasts a message for a given actor to only players that actor has been revealed.
/// </summary>
/// <param name="message">The message to broadcast.</param>
/// <param name="actor">The actor.</param>
public void BroadcastIfRevealed(Func<Player, GameMessage> message, Actor actor)
{
BroadcastOperation(player => player.RevealedObjects.ContainsKey(actor.GlobalID), player => player.InGameClient.SendMessage(message(player)));
}
/// <summary>
/// Broadcasts a message to all players in the world.
/// </summary>
/// <param name="message"></param>
public void BroadcastGlobal(Func<Player, GameMessage> message)
{
BroadcastOperation(player => player.InGameClient.SendMessage(message(player)));
}
/// <summary>
/// Broadcasts a message to all players in the range of given actor.
/// </summary>
/// <param name="message">The message to broadcast.</param>
/// <param name="actor">The actor.</param>
public void BroadcastInclusive(Func<Player, GameMessage> message, Actor actor, float? radius = null)
{
var players = actor.GetPlayersInRange(radius);
foreach (var player in players)
{
player.InGameClient.SendMessage(message(player));
}
}
/// <summary>
/// Broadcasts a message to all players in the range of given actor, but not the player itself if actor is the player.
/// </summary>
/// <param name="message">The message to broadcast.</param>
/// <param name="actor">The actor.</param>
public void BroadcastExclusive(Func<Player, GameMessage> message, Actor actor, bool global = false)
{
var players = actor.GetPlayersInRange();
if (global) players = Players.Values.ToList();
foreach (var player in players.Where(player => player != actor))
{
if (player.RevealedObjects.ContainsKey(actor.GlobalID)) //revealed only!
player.InGameClient.SendMessage(message(player));
}
}
#endregion
#region reveal logic
/// <summary>
/// Reveals the world to given player.
/// </summary>
/// <param name="player">The player.</param>
/// <returns></returns>
public bool Reveal(Player player)
{
lock (player.RevealedObjects)
{
if (player.RevealedObjects.ContainsKey(GlobalID))
return false;
int sceneGridSize = SNO.IsUsingZoltCustomGrid() ? 100 : 60;
player.InGameClient.SendMessage(new RevealWorldMessage() // Reveal world to player
{
WorldID = GlobalID,
WorldSNO = WorldSNO.Id,
OriginX = 540,
OriginY = -600,
StitchSizeInFeetX = sceneGridSize,
StitchSizeInFeetY = sceneGridSize,
WorldSizeInFeetX = 5040,
WorldSizeInFeetY = 5040,
snoDungeonFinderSourceWorld = -1
});
player.InGameClient.SendMessage(new WorldStatusMessage { WorldID = GlobalID, Field1 = false });
//*
player.InGameClient.SendMessage(new WorldSyncedDataMessage
{
WorldID = GlobalID,
SyncedData = new WorldSyncedData
{
SnoWeatherOverride = -1,
WeatherIntensityOverride = 0,
WeatherIntensityOverrideEnd = 0
}
});
//*/
player.RevealedObjects.Add(GlobalID, GlobalID);
return true;
}
}
/// <summary>
/// Unreveals the world to player.
/// </summary>
/// <param name="player">The player.</param>
/// <returns></returns>
public bool Unreveal(Player player)
{
if (!player.RevealedObjects.ContainsKey(GlobalID)) return false;
foreach (var scene in Scenes.Values) scene.Unreveal(player);
player.RevealedObjects.Remove(GlobalID);
player.InGameClient.SendMessage(new WorldStatusMessage { WorldID = GlobalID, Field1 = true });
player.InGameClient.SendMessage(new PrefetchDataMessage(Opcodes.PrefetchWorldMessage) { SNO = WorldSNO.Id });
//player.InGameClient.SendMessage(new WorldDeletedMessage() { WorldID = this.GlobalID });
return true;
}
#endregion
#region actor enter & leave functionality
/// <summary>
/// Allows an actor to enter the world.
/// </summary>
/// <param name="actor">The actor entering the world.</param>
public void Enter(Actor actor)
{
AddActor(actor);
actor.OnEnter(this);
// reveal actor to player's in-range.
foreach (var player in actor.GetPlayersInRange())
{
actor.Reveal(player);
}
//Убираем балки с проходов
if (SNO == WorldSno.trout_town)
{
foreach (var boarded in GetActorsBySNO(ActorSno._trout_oldtristram_cellardoor_boarded))
boarded.Destroy();
foreach (var boarded in GetActorsBySNO(ActorSno._trout_oldtristram_cellardoor_rubble))
boarded.Destroy();
}
}
/// <summary>
/// Allows an actor to leave the world.
/// </summary>
/// <param name="actor">The actor leaving the world.</param>
public void Leave(Actor actor)
{
actor.OnLeave(this);
foreach (var player in Players.Values)
{
actor.Unreveal(player);
}
if (HasActor(actor.GlobalID))
RemoveActor(actor);
if (!(actor is Player)) return; // if the leaving actors is a player, unreveal the actors revealed to him contained in the world.
var revealedObjects = (actor as Player).RevealedObjects.Keys.ToList(); // list of revealed actors.
foreach (var obj_id in revealedObjects)
{
var obj = GetActorByGlobalId(obj_id);
//if (obj != actor) // do not unreveal the player itself.
try
{
if (obj != null)
obj.Unreveal(actor as Player);
//System.Threading.Tasks.Task.Delay(5).Wait();
}
catch { }
}
}
#endregion
#region Отображение только конкретной итерации NPC
public Actor ShowOnlyNumNPC(ActorSno SNO, int Num)
{
Actor Setted = null;
foreach (var actor in GetActorsBySNO(SNO))
{
var isVisible = actor.NumberInWorld == Num;
if (isVisible)
Setted = actor;
actor.Hidden = !isVisible;
actor.SetVisible(isVisible);
foreach (var plr in Players.Values)
{
if (isVisible) actor.Reveal(plr); else actor.Unreveal(plr);
}
}
return Setted;
}
#endregion
#region monster spawning & item drops
/// <summary>
/// Spawns a monster with given SNOId in given position.
/// </summary>
/// <param name="monsterSno">The SNOId of the monster.</param>
/// <param name="position">The position to spawn it.</param>
public Actor SpawnMonster(ActorSno monsterSno, Vector3D position)
{
if (monsterSno == ActorSno.__NONE) return null;
Logger.MethodTrace($"Spawning monster {monsterSno} at {position}");
var monster = ActorFactory.Create(this, monsterSno, new TagMap());
if (monster == null) return null;
monster.EnterWorld(position);
if (monster.AnimationSet == null) return monster;
var animationTag = new[] { AnimationSetKeys.Spawn, AnimationSetKeys.Spawn2 }.FirstOrDefault(x => monster.AnimationSet.TagMapAnimDefault.ContainsKey(x));
if (animationTag != null)
{
monster.World.BroadcastIfRevealed(plr => new PlayAnimationMessage
{
ActorID = monster.DynamicID(plr),
AnimReason = 5,
UnitAniimStartTime = 0,
tAnim = new PlayAnimationMessageSpec[]
{
new()
{
Duration = 150,
AnimationSNO = monster.AnimationSet.TagMapAnimDefault[animationTag],
PermutationIndex = 0,
Speed = 1
}
}
}, monster);
}
return monster;
}
private Queue<Queue<Action>> _flippyTimers = new();
private const int FlippyDurationInTicks = 10;
private const int FlippyMaxDistanceManhattan = 10; // length of one side of the square around the player where the item will appear
private const int FlippyDefaultFlippy = 0x6d82; // g_flippy.prt
public void SpawnItem(Actor source, Player player, int GBid)
{
var item = ItemGenerator.CookFromDefinition(player.World, ItemGenerator.GetItemDefinition(GBid));
if (item == null) return;
player.GroundItems[item.GlobalID] = item; // FIXME: Hacky. /komiga
DropItem(source, null, item);
}
[Obsolete("Isn't used anymore. Is it useful?")]
public void PlayPieAnimation(Actor actor, Actor user, int powerSNO, Vector3D targetPosition)
{
BroadcastIfRevealed(plr => new ACDTranslateDetPathPieWedgeMessage
{
ann = (int)actor.DynamicID(plr),
StartPos = user.Position,
FirstTagetPos = user.Position,
MoveFlags = 9,
AnimTag = 1,
PieData = new DPathPieData
{
Field0 = targetPosition,
Field1 = 1,
Field2 = 1,
Field3 = 1
},
Field6 = 1f,
}, actor);
}
public void PlayCircleAnimation(Actor actor, Actor User, int PowerSNO, Vector3D TargetPosition)
{
BroadcastIfRevealed(plr => new ACDTranslateDetPathSinMessage
{
ActorID = actor.DynamicID(plr),
DPath = 6,
// 0 - crashes client
// 1 - random scuttle (charged bolt effect)
// 2 - random movement, random movement pauses (toads hopping)
// 3 - clockwise spiral
// 4 - counter-clockwise spiral
Seed = 1,
Carry = 1,
TargetPostition = TargetPosition,
Angle = MovementHelpers.GetFacingAngle(User.Position, TargetPosition),
StartPosition = User.Position,
MoveFlags = 1,
AnimTag = 1,
PowerSNO = PowerSNO,
Var0Int = 1,
Var0Fl = 1f,
SinData = new DPathSinData
{
annOwner = (int)actor.DynamicID(plr),
SinIncAccel = 0f,
LateralMaxDistance = 9f,
LateralStartDistance = 1f,
OOLateralDistanceToScale = 5f,
SinIncPerTick = 1f,
Speed = 0.5f
},
SpeedMulti = 0.1f,
}, actor);
}
public void PlayZigAnimation(Actor actor, Actor User, int PowerSNO, Vector3D TargetPosition)
{
BroadcastIfRevealed(plr => new ACDTranslateFacingMessage
{
ActorId = actor.DynamicID(plr),
Angle = MovementHelpers.GetFacingAngle(User.Position, TargetPosition),
TurnImmediately = true
}, actor);
BroadcastIfRevealed(plr => new ACDTranslateDetPathSinMessage
{
ActorID = actor.DynamicID(plr),
DPath = 5,
Seed = 1,
Carry = 1,
TargetPostition = TargetPosition,
Angle = MovementHelpers.GetFacingAngle(User.Position, TargetPosition),
StartPosition = User.Position,
MoveFlags = 1,
AnimTag = 1,
PowerSNO = PowerSNO,
Var0Int = 1,
Var0Fl = 1f,
SinData = new DPathSinData
{
annOwner = (int)actor.DynamicID(plr),
SinIncAccel = 0f,
LateralMaxDistance = 9f,
LateralStartDistance = 1f,
OOLateralDistanceToScale = 5f,
SinIncPerTick = 0.2f,
Speed = 0.5f
},
SpeedMulti = 1f,
}, actor);
}
public void PlayReverSpiralAnimation(Actor actor, Actor User, int PowerSNO, Vector3D TargetPosition)
{
BroadcastIfRevealed(plr => new ACDTranslateFacingMessage
{
ActorId = actor.DynamicID(plr),
Angle = MovementHelpers.GetFacingAngle(User.Position, TargetPosition),
TurnImmediately = true
}, actor);
BroadcastIfRevealed(plr => new ACDTranslateDetPathSinMessage
{
ActorID = actor.DynamicID(plr),
DPath = 4,
// 0 - crashes client
// 1 - random scuttle (charged bolt effect)
// 2 - random movement, random movement pauses (toads hopping)
// 3 - clockwise spiral
// 4 - counter-clockwise spiral
Seed = 1,
Carry = 1,
TargetPostition = TargetPosition,
Angle = MovementHelpers.GetFacingAngle(User.Position, TargetPosition),
StartPosition = User.Position,
MoveFlags = 1,
AnimTag = 1,
PowerSNO = PowerSNO,
Var0Int = 1,
Var0Fl = 1f,
SinData = new DPathSinData
{
annOwner = (int)actor.DynamicID(plr),
SinIncAccel = 0.2f,
LateralMaxDistance = 0.1f,
LateralStartDistance = 0.1f,
OOLateralDistanceToScale = 0.1f,
SinIncPerTick = 1f,
Speed = 1f
},
SpeedMulti = 1f,
}, actor);
}
public void PlaySpiralAnimation(Actor actor, Actor User, int PowerSNO, Vector3D TargetPosition)
{
BroadcastIfRevealed(plr => new ACDTranslateFacingMessage
{
ActorId = actor.DynamicID(plr),
Angle = MovementHelpers.GetFacingAngle(User.Position, TargetPosition),
TurnImmediately = true
}, actor);
BroadcastIfRevealed(plr => new ACDTranslateDetPathSinMessage
{
ActorID = actor.DynamicID(plr),
DPath = 3,
// 0 - crashes client
// 1 - random scuttle (charged bolt effect)
// 2 - random movement, random movement pauses (toads hopping)
// 3 - clockwise spiral
// 4 - counter-clockwise spiral
Seed = 1,
Carry = 1,
TargetPostition = TargetPosition,
Angle = MovementHelpers.GetFacingAngle(User.Position, TargetPosition),
StartPosition = User.Position,
MoveFlags = 1,
AnimTag = 1,
PowerSNO = PowerSNO,
Var0Int = 1,
Var0Fl = 1f,
SinData = new DPathSinData
{
annOwner = (int)actor.DynamicID(plr),
SinIncAccel = 0.2f,
LateralMaxDistance = 0.1f,
LateralStartDistance = 0.1f,
OOLateralDistanceToScale = 0.1f,
SinIncPerTick = 1f,
Speed = 1f
},
SpeedMulti = 1f,
}, actor);
}
public Item SpawnRandomEquip(Actor source, Player player, int forceQuality = -1, int forceLevel = -1,
GameBalance.ItemTypeTable type = null, bool canBeUnidentified = true, ToonClass toonClass = ToonClass.Unknown)
{
Logger.MethodTrace($"quality {forceQuality}");
if (player != null)
{
int level = (forceLevel > 0 ? forceLevel : source.Attributes[GameAttributes.Level]);
if (toonClass == ToonClass.Unknown && type == null)
{
var item = ItemGenerator.GenerateRandomEquip(player, level, forceQuality, forceQuality, canBeUnidentified: canBeUnidentified);
if (item == null) return null;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
return item;
}
else
{
var item = ItemGenerator.GenerateRandomEquip(player, level, forceQuality, forceQuality, type: type,ownerClass: toonClass, canBeUnidentified: canBeUnidentified);
if (item == null) return null;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
return item;
}
}
return null;
}
public void SpawnRandomLegOrSetEquip(Actor source, Player player)
{
//Logger.MethodTrace("quality {0}", forceQuality);
if (player != null)
{
var item = ItemGenerator.GenerateLegOrSetRandom(player);
if (item == null) return;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
}
}
public void SpawnRandomCraftItem(Actor source, Player player)
{
if (player != null)
{
var item = ItemGenerator.GenerateRandomCraftItem(player, source.Attributes[GameAttributes.Level], true);
if (item == null) return;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
if (source.Attributes[GameAttributes.Level] >= Program.MaxLevel)
{
item = ItemGenerator.GenerateRandomCraftItem(player, 35);
if (item == null) return;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
item = ItemGenerator.GenerateRandomCraftItem(player, 55);
if (item == null) return;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
}
}
}
public void SpawnRandomUniqueGem(Actor source, Player player)
{
if (player != null)
{
var item = ItemGenerator.GenerateRandomUniqueGem(player);
if (item == null) return;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
}
}
public void SpawnRandomGem(Actor source, Player player)
{
if (player != null && //player.JewelerUnlocked &&
player.Attributes[GameAttributes.Level] >= 15)
{
var item = ItemGenerator.GenerateRandomGem(player, source.Attributes[GameAttributes.Level], source is Goblin);
if (item == null) return;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
}
}
public void SpawnRandomPotion(Actor source, Player player)
{
if (player != null && !player.Inventory.HaveEnough(StringHashHelper.HashItemName("HealthPotionBottomless"), 1))
{
var item = ItemGenerator.GenerateRandomPotion(player);
if (item == null) return;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
}
}
public void SpawnEssence(Actor source, Player player)
{
int essence = (source.Attributes[GameAttributes.Level] > 60 ? 2087837753 : -152489231);
if (player != null)
{
var item = ItemGenerator.CookFromDefinition(player.World, ItemGenerator.GetItemDefinition(essence)); //Crafting_Demonic_Reagent
if (item == null) return;
player.GroundItems[item.GlobalID] = item;
DropItem(source, null, item);
}
}
/// <summary>
/// Spanws gold for given player.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="position">The position for drop.</param>
public void SpawnGold(Actor source, Player player, int Min = -1)
{
int amount = (int)(LootManager.GetGoldAmount(player.Attributes[GameAttributes.Level]) * Game.GoldModifier * GameModsConfig.Instance.Rate.Gold);
if (Min != -1)
amount += Min;
var item = ItemGenerator.CreateGold(player, amount); // somehow the actual ammount is not shown on ground /raist.
player.GroundItems[item.GlobalID] = item;
DropItem(source, player, item);
}
/// <summary>
/// Spanws blood shards for given player.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="position">The position for drop.</param>
public void SpawnBloodShards(Actor source, Player player, int forceAmount = 0)
{
int amount = LootManager.GetBloodShardsAmount(Game.Difficulty + 3);
if (forceAmount == 0 && amount == 0) return; //don't drop shards on Normal
var item = ItemGenerator.CreateBloodShards(player, forceAmount > 0 ? forceAmount : amount); // somehow the actual ammount is not shown on ground /raist.
player.GroundItems[item.GlobalID] = item;
DropItem(source, player, item);
}
/// <summary>
/// Returns the first actor found with a given sno id
/// </summary>
/// <param name="sno"></param>
/// <returns></returns>
public Actor GetActorBySNO(ActorSno sno, bool onlyVisible = false)
{
return Actors.Values.FirstOrDefault(x => x.SNO == sno && (!onlyVisible || (onlyVisible && x.Visible && !x.Hidden)));
}
public List<Portal> GetPortalsByLevelArea(int la)
{
List<Portal> portals = new List<Portal>();
foreach (var actor in Actors.Values)
{
if (actor is Portal)
if ((actor as Portal).Destination != null)
if ((actor as Portal).Destination.DestLevelAreaSNO == la)
{
bool alreadyAdded = false;
foreach (var pt in portals)
if (pt.Position == actor.Position) alreadyAdded = true;
if (!alreadyAdded)
portals.Add(actor as Portal);
}
}
return portals;
}
/// <summary>
/// Returns all actors matching one of SNOs
/// </summary>
/// <param name="sno"></param>
/// <returns></returns>
public List<Actor> GetActorsBySNO(params ActorSno[] sno)
{
return Actors.Values.Where(x => sno.Contains(x.SNO)).ToList();
}
/// <summary>
/// Returns true if any actors exist under a well defined group
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
public bool HasActorsInGroup(string group)
{
var groupHash = StringHashHelper.HashItemName(group);
foreach (var actor in Actors.Values)
{
if (actor.Tags != null)
if (actor.Tags.ContainsKey(MarkerKeys.Group1Hash))
if (actor.Tags[MarkerKeys.Group1Hash] == groupHash) return true;
}
return false;
}
/// <summary>
/// Returns all actors matching a group
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
public List<Actor> GetActorsInGroup(string group)
{
List<Actor> matchingActors = new List<Actor>();
var groupHash = StringHashHelper.HashItemName(group);
foreach (var actor in Actors.Values)
{
if (actor.Tags != null)
if (actor.Tags.ContainsKey(MarkerKeys.Group1Hash))
if (actor.Tags[MarkerKeys.Group1Hash] == groupHash) matchingActors.Add(actor);
}
return matchingActors;
}
public List<Actor> GetActorsInGroup(int hash)
{
List<Actor> matchingActors = new List<Actor>();
foreach (var actor in Actors.Values)
{
if (actor.Tags != null)
if (actor.Tags.ContainsKey(MarkerKeys.Group1Hash))
if (actor.Tags[MarkerKeys.Group1Hash] == hash) matchingActors.Add(actor);
}
return matchingActors;
}
/// <summary>
/// Spanws a health-globe for given player.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="position">The position for drop.</param>
public void SpawnHealthGlobe(Actor source, Player player, Vector3D position)
{
// TODO: Health-globe should be spawned for all players in range. /raist.
var item = ItemGenerator.CreateHealthGlobe(player, RandomHelper.Next(10, 15));
DropItem(source, player, item);
}
public void SpawnArcaneGlobe(Actor source, Player player, Vector3D position)
{
var item = ItemGenerator.CreateArcaneGlobe(player);
DropItem(source, player, item);
}
public void SpawnPowerGlobe(Actor source, Player player, Vector3D position)
{
var item = ItemGenerator.CreatePowerGlobe(player);
DropItem(source, player, item);
}
/// <summary>
/// Update the flippy animations and remove them once they have timed out
/// </summary>
/// <param name="tickCounter"></param>
private void UpdateFlippy(int tickCounter)
{
if (_flippyTimers.Peek() == null)
_flippyTimers.Dequeue();
if (_flippyTimers.Peek().Count() == 2)
_flippyTimers.Peek().Dequeue().Invoke();
else
{
_flippyTimers.Dequeue().Dequeue().Invoke();
if (_flippyTimers.Any())
_flippyTimers.Peek().Dequeue().Invoke();
}
}
/// <summary>
/// Drops a given item to a random position close to the player
/// </summary>
/// <param name="player">Player to which to reveal the item</param>
/// <param name="item">Item to reveal</param>
public void DropItem(Player player, Item item)
{
DropItem(player, player, item);
}
/// <summary>
/// Drops a given item to a random position close to a source actor
/// </summary>
/// <param name="source">Source actor of the flippy animation</param>
/// <param name="player">Player to which to reveal the item</param>
/// <param name="item">Item to reveal</param>
public void DropItem(Actor source, Player player, Item item)
{
// Get a random location close to the source
float x = (float)(RandomHelper.NextDouble() - 0.5) * FlippyMaxDistanceManhattan;
float y = (float)(RandomHelper.NextDouble() - 0.5) * FlippyMaxDistanceManhattan;
item.Position = source.Position + new Vector3D(x, y, 0);
if (worldData.DynamicWorld)
item.Position.Z = GetZForLocation(item.Position, source.Position.Z);
item.Unstuck();
// Items send either only a particle effect or default particle and either FlippyTag.Actor or their own actorsno
int particleSNO = -1;
int actorSNO = -1;
if (item.SnoFlippyParticle != null)
{
particleSNO = item.SnoFlippyParticle.Id;
}
else
{
actorSNO = item.SnoFlippyActory == null ? -1 : item.SnoFlippyActory.Id;
particleSNO = FlippyDefaultFlippy;
}
var queue = new Queue<Action>();
queue.Enqueue(() => {
BroadcastIfRevealed(plr => new FlippyMessage
{
ActorID = (int)source.GlobalID,
Destination = item.Position,
SNOFlippyActor = actorSNO,
SNOParticleEffect = particleSNO
}, source);
});
queue.Enqueue(() => item.Drop(null, item.Position));
_flippyTimers.Enqueue(queue);
}
#endregion
#region collections managemnet
/// <summary>
/// Adds given scene to world.
/// </summary>
/// <param name="scene">The scene to add.</param>
public void AddScene(Scene scene)
{
if (scene.GlobalID == 0 || HasScene(scene.GlobalID))
throw new Exception($"Scene has an invalid ID or was already present (ID = {scene.GlobalID})");
Scenes.TryAdd(scene.GlobalID, scene); // add to scenes collection.
QuadTree.Insert(scene); // add it to quad-tree too.
}
/// <summary>
/// Removes given scene from world.
/// </summary>
/// <param name="scene">The scene to remove.</param>
public void RemoveScene(Scene scene)
{
if (scene.GlobalID == 0 || !HasScene(scene.GlobalID))
throw new Exception($"Scene has an invalid ID or was not present (ID = {scene.GlobalID})");
Scenes.TryRemove(scene.GlobalID, out _); // remove it from scenes collection.
QuadTree.Remove(scene); // remove from quad-tree too.
}
/// <summary>
/// Returns the scene with given dynamicId.
/// </summary>
/// <param name="dynamicID">The dynamicId of the scene.</param>
/// <returns></returns>
public Scene GetScene(uint dynamicID)
{
Scenes.TryGetValue(dynamicID, out var scene);
return scene;
}
internal object GetActorByDynamicId(uint actor)
{
throw new NotImplementedException();
}
/// <summary>
/// Returns true if world contains a scene with given dynamicId.
/// </summary>
/// <param name="dynamicID">The dynamicId of the scene.</param>
/// <returns><see cref="bool"/></returns>
public bool HasScene(uint dynamicID)
{
return Scenes.ContainsKey(dynamicID);
}
/// <summary>
/// Adds given actor to world.
/// </summary>
/// <param name="actor">The actor to add.</param>
private void AddActor(Actor actor)
{
if (actor.GlobalID == 0 || HasActor(actor.GlobalID))
{
Logger.Warn("Actor has an invalid ID or was already present (ID = {0})", actor.GlobalID);
//actor.DynamicID = this.NewActorID;
return;
}
Actors.TryAdd(actor.GlobalID, actor); // add to actors collection.
QuadTree.Insert(actor); // add it to quad-tree too.
if (actor.ActorType == ActorType.Player) // if actor is a player, add it to players collection too.
AddPlayer((Player)actor);
}
/// <summary>
/// Removes given actor from world.
/// </summary>
/// <param name="actor">The actor to remove.</param>
private void RemoveActor(Actor actor)
{
if (actor.GlobalID == 0 || !Actors.ContainsKey(actor.GlobalID))
throw new Exception($"Actor has an invalid ID or was not present (ID = {actor.GlobalID})");
Actors.TryRemove(actor.GlobalID, out _); // remove it from actors collection.
QuadTree.Remove(actor); // remove from quad-tree too.
if (actor.ActorType == ActorType.Player) // if actors is a player, remove it from players collection too.
RemovePlayer((Player)actor);
}
public Actor GetActorByGlobalId(uint globalID)
{
Actors.TryGetValue(globalID, out var actor);
return actor;
}
public uint GetGlobalId(Player plr, uint dynamicID)
{
try
{
return plr.RevealedObjects.Single(a => a.Value == dynamicID).Key;
}
catch
{
//Logger.Warn("Ошибка с актором - ID {0}", dynamicID);
return dynamicID;
}
}
/// <summary>
/// Returns the actor with given dynamicId.
/// </summary>
/// <param name="dynamicID">The dynamicId of the actor.</param>
/// <param name="matchType">The actor-type.</param>
/// <returns></returns>
public Actor GetActorByGlobalId(uint dynamicID, ActorType matchType)
{
var actor = GetActorByGlobalId(dynamicID);
if (actor != null)
{
if (actor.ActorType == matchType)
return actor;
Logger.Warn("Attempted to get actor ID {0} as a {1}, whereas the actor is type {2}",
dynamicID, Enum.GetName(typeof(ActorType), matchType), Enum.GetName(typeof(ActorType), actor.ActorType));
}
return null;
}
/// <summary>
/// Returns true if the world has an actor with given dynamicId.
/// </summary>
/// <param name="dynamicID">The dynamicId of the actor.</param>
/// <returns><see cref="bool"/></returns>
public bool HasActor(uint dynamicID)
{
return Actors.ContainsKey(dynamicID);
}
/// <summary>
/// Returns true if the world has an actor with given dynamicId and type.
/// </summary>
/// <param name="dynamicID">The dynamicId of the actor.</param>
/// <param name="matchType">The type of the actor.</param>
/// <returns></returns>
public bool HasActor(uint dynamicID, ActorType matchType)
{
var actor = GetActorByGlobalId(dynamicID, matchType);
return actor != null;
}
/// <summary>
/// Returns actor instance with given type.
/// </summary>
/// <typeparam name="T">Type of the actor.</typeparam>
/// <returns>Actor</returns>
public T GetActorInstance<T>() where T : Actor
{
return Actors.Values.OfType<T>().FirstOrDefault();
}
/// <summary>
/// Adds given player to world.
/// </summary>
/// <param name="player">The player to add.</param>
private bool AddPlayer(Player player)
{
if (player == null)
throw new Exception($"Player in world {SNO} is null and cannot be removed.");
if (player.GlobalID == 0 || HasPlayer(player.GlobalID))
throw new Exception($"Player has an invalid ID or was already present (ID = {player.GlobalID})");
return Players.TryAdd(player.GlobalID, player); // add it to players collection.
}
/// <summary>
/// Removes given player from world.
/// </summary>
/// <param name="player"></param>
private bool RemovePlayer(Player player)
{
if (player == null)
throw new Exception($"Player in world {SNO} is null and cannot be removed.");
if (player.GlobalID == 0 || !Players.ContainsKey(player.GlobalID))
throw new Exception($"Player has an invalid ID or was not present (ID = {player.GlobalID})");
return Players.TryRemove(player.GlobalID, out _); // remove it from players collection.
}
/// <summary>
/// Returns player with a given predicate
/// </summary>
/// <param name="predicate">Predicate to find player</param>
/// <param name="player">Player result</param>
/// <returns>Whether the player was found.</returns>
public bool TryGetPlayer(Func<Player, bool> predicate, out Player player)
{
player = Players.Values.FirstOrDefault(predicate);
return player != null;
}
/// <summary>
/// Returns true if world contains a player with given dynamicId.
/// </summary>
/// <param name="dynamicID">The dynamicId of the player.</param>
/// <returns><see cref="bool"/></returns>
public bool HasPlayer(uint dynamicID) => Players.ContainsKey(dynamicID);
/// <summary>
/// Returns item with given dynamicId.
/// </summary>
/// <param name="dynamicID">The dynamicId of the item.</param>
/// <returns></returns>
public Item GetItem(uint dynamicID) => (Item)GetActorByGlobalId(dynamicID, ActorType.Item);
/// <summary>
/// Returns true if world contains a monster with given dynamicId.
/// </summary>
/// <param name="dynamicID">The dynamicId of the monster.</param>
/// <returns><see cref="bool"/></returns>
public bool HasMonster(uint dynamicID) => HasActor(dynamicID, ActorType.Monster);
/// <summary>
/// Returns true if world contains an item with given dynamicId.
/// </summary>
/// <param name="dynamicID">The dynamicId of the item.</param>
/// <returns><see cref="bool"/></returns>
public bool HasItem(uint dynamicID) => HasActor(dynamicID, ActorType.Item);
#endregion
#region misc-queries
/// <summary>
/// Returns StartingPoint with given id.
/// </summary>
/// <param name="id">The id of the StartingPoint.</param>
/// <returns><see cref="StartingPoint"/></returns>
public StartingPoint GetStartingPointById(int id) => Actors.Values.OfType<StartingPoint>().Where(sp => sp.TargetId == id).ToList().FirstOrDefault();
public Actor FindActorAt(ActorSno actorSno, Vector3D position, float radius = 3.0f)
{
var proximityCircle = new Circle(position.X, position.Y, radius);
var actors = QuadTree.Query<Actor>(proximityCircle);
foreach (var actor in actors)
if (actor.Attributes[GameAttributes.Disabled] == false && actor.Attributes[GameAttributes.Gizmo_Has_Been_Operated] == false && actor.SNO == actorSno) return actor;
return null;
}
/// <summary>
/// Returns WayPoint with given id.
/// </summary>
/// <param name="id">The id of the WayPoint</param>
/// <returns><see cref="Waypoint"/></returns>
public Waypoint GetWayPointById(int id) => Actors.Values.OfType<Waypoint>().FirstOrDefault(waypoint => waypoint.WaypointId == id);
public Waypoint[] GetAllWaypoints() => Actors.Values.OfType<Waypoint>().ToArray();
public Waypoint[] GetAllWaypointsInWorld(WorldSno worldSno) => Actors.Values.OfType<Waypoint>().Where(waypoint => waypoint.World.SNO == worldSno).ToArray();
public Waypoint[] GetAllWaypointsInWorld(World world) => Actors.Values.OfType<Waypoint>().Where(waypoint => waypoint.World == world).ToArray();
#endregion
#region destroy, ctor, finalizer
public override void Destroy()
{
Logger.Trace($"$[red]$Destroying$[/]$ World #{GlobalID} $[underline red]${SNO}$[/]$");
// TODO: Destroy all objects @iamdroppy - solution below added for testing on 21/01/2023
// foreach (var actor in Actors.Values)
// try
// {
// actor.Destroy();
// }
// catch {}
//
// foreach (var player in Players.Values)
// try
// {
// player.Destroy();
// }
// catch{}
// foreach (var portal in Portals)
// try
// {
// portal.Destroy();
// }
// catch{}
// TODO: Destroy pre-generated tile set
worldData = null;
//Game game = this.Game;
Game = null;
//game.EndTracking(this);
}
#endregion
public bool CheckLocationForFlag(Vector3D location, DiIiS_NA.Core.MPQ.FileFormats.Scene.NavCellFlags flags)
{
// We loop Scenes as its far quicker than looking thru the QuadTree - DarkLotus
foreach (Scene s in Scenes.Values)
{
if (s.Bounds.Contains(location.X, location.Y))
{
Scene scene = s;
if (s.Parent != null) { scene = s.Parent; }
if (s.Subscenes.Count > 0)
{
foreach (var subScene in s.Subscenes.Where(subScene => subScene.Bounds.Contains(location.X, location.Y)))
{
scene = subScene;
}
}
int x = (int)((location.X - scene.Bounds.Left) / 2.5f);
int y = (int)((location.Y - scene.Bounds.Top) / 2.5f);
int total = (y * scene.NavMesh.SquaresCountX) + x;
if (total < 0 || total > scene.NavMesh.NavMeshSquareCount)
{
Logger.Error("Navmesh overflow!");
return false;
}
try
{
return (scene.NavMesh.Squares[total].Flags & flags) == flags;
}
catch { }
}
}
return false;
}
public float GetZForLocation(Vector3D location, float defaultZ)
{
foreach (Scene s in Scenes.Values.Where(s => s.Bounds.Contains(location.X, location.Y)))
{
Scene scene = s;
if (s.Parent != null)
{
scene = s.Parent;
}
if (s.Subscenes.Count > 0)
{
foreach (var subScene in s.Subscenes)
{
if (subScene.Bounds.Contains(location.X, location.Y))
{
scene = subScene;
}
}
}
int x = (int)((location.X - scene.Bounds.Left) / 2.5f);
int y = (int)((location.Y - scene.Bounds.Top) / 2.5f);
int total = (y * scene.NavMesh.SquaresCountX) + x;
if (total < 0 || total > scene.NavMesh.NavMeshSquareCount)
{
Logger.Error("Navmesh overflow!");
return defaultZ;
}
try
{
return scene.NavMesh.Squares[total].Z;
}
catch
{
return defaultZ;
}
}
return defaultZ;
}
[Obsolete("Isn't used anymore")] // made obsolete by @iamdroppy on 28/01/2023
public bool CheckRayPath(Vector3D start, Vector3D destination)
{
var proximity = new RectangleF(start.X - 1f, start.Y - 1f, 2f, 2f);
var scenes = QuadTree.Query<Scene>(proximity);
if (scenes.Count == 0) return false;
if (scenes.Count == 2) // What if it's a subscene? /fasbat
{
if (scenes[1].ParentChunkID != 0xFFFFFFFF)
{
}
}
return true;
}
public override string ToString()
{
return $"[World] SNOId: {WorldSNO.Id} GlobalId: {GlobalID} Name: {WorldSNO.Name}";
}
public ImmutableArray<Door> GetAllDoors() =>
Actors.Select(a => a.Value).Where(a => a is Door).Cast<Door>().ToImmutableArray();
public ImmutableArray<Door> GetAllDoors(ActorSno sno) =>
Actors.Select(a => a.Value).Where(a => a is Door && a.SNO == sno).Cast<Door>().ToImmutableArray();
public ImmutableArray<Door> OpenAllDoors()
{
List<Door> openedDoors = new();
var doors = GetAllDoors();
foreach (var door in doors)
{
openedDoors.Add(door);
door.Open();
}
return openedDoors.ToImmutableArray();
}
}
}