Partial of HorizonGame that holds the map door state. Syncs which doors are open and which are locked from the host, handles open/close and staff lock RPCs, and persists locks through the Backend API or a local JSON file when offline.
namespace HorizonRP;
/// <summary>
/// L'état des portes de la map convertie (World/MapDoor.cs) : ouvert ou fermé, et verrouillé par le
/// staff, tenu par l'hôte et partagé avec tous. Ici, sur HorizonGame, avec les autres états partagés
/// du jeu. Les verrous sont gardés en base (hz_map_locks, par MapDoor.Key) et reposés au démarrage.
/// </summary>
public sealed partial class HorizonGame
{
/// <summary>Les portes ouvertes : numéro → côté (1 ou -1). Une porte absente est fermée.</summary>
[Sync( SyncFlags.FromHost )] public NetDictionary<int, int> MapDoors { get; set; } = new();
/// <summary>Les portes verrouillées (numéros) : elles ne s'ouvrent plus.</summary>
[Sync( SyncFlags.FromHost )] public NetList<int> LockedMapDoors { get; set; } = new();
/// <summary>Hors ligne (éditeur), les verrous vont dans ce fichier des données.</summary>
const string MapLocksFile = "verrous_portes.json";
bool mapLocksLoaded;
RealTimeSince sinceMapLocksTry = 60;
/// <summary>Un joueur ouvre ou ferme une porte de la map (touche E) ; l'hôte vérifie qu'il est à côté.</summary>
[Rpc.Host]
public void RequestMapDoor( int index )
{
var player = RoleplayPlayer.All.FirstOrDefault( x => Rpc.Caller is not null && x.Network.OwnerId == Rpc.Caller.Id );
var door = Scene.GetAllComponents<MapDoor>().FirstOrDefault( x => x.Index == index );
if ( player is null || door is null || player.Downed || player.Restrained )
return;
if ( player.WorldPosition.Distance( door.WorldTransform.PointToWorld( door.PanelCenter ) ) > MapDoor.Reach + 120 )
return;
if ( MapDoors.ContainsKey( index ) )
MapDoors.Remove( index );
else if ( LockedMapDoors.Contains( index ) )
// Fermée à clé : la poignée résiste, et tout le monde autour l'entend.
PlayMapDoorDenied( index );
else
MapDoors[index] = door.SideAwayFrom( player.WorldPosition );
}
/// <summary>Chez tous : quelqu'un a essayé d'ouvrir une porte verrouillée. Elle tremble, la poignée résiste.</summary>
[Rpc.Broadcast( NetFlags.HostOnly )]
void PlayMapDoorDenied( int index )
{
if ( Scene.GetAllComponents<MapDoor>().FirstOrDefault( x => x.Index == index ) is not { } door )
return;
door.Rattle();
Sounds.PlayFileAt( MapDoor.LockedSound, door.SoundPosition, 0.8f );
}
/// <summary>
/// G maintenu sur une porte (MapDoor.UpdateLockHold) : la verrouiller ou la déverrouiller, pour
/// de bon. Le staff seulement pour l'instant ; les clés des joueurs viendront. Une porte ouverte
/// se ferme en se verrouillant.
/// </summary>
[Rpc.Host]
public void RequestMapDoorLock( int index, bool locked )
{
var player = RoleplayPlayer.All.FirstOrDefault( x => Rpc.Caller is not null && x.Network.OwnerId == Rpc.Caller.Id );
var door = Scene.GetAllComponents<MapDoor>().FirstOrDefault( x => x.Index == index );
if ( player is null || door is null || !player.IsStaff || door.Key is null )
return;
if ( player.WorldPosition.Distance( door.SoundPosition ) > MapDoor.Reach + 120 )
return;
if ( locked == LockedMapDoors.Contains( index ) )
return;
if ( locked )
{
MapDoors.Remove( index );
LockedMapDoors.Add( index );
}
else
LockedMapDoors.Remove( index );
player.Notify( locked ? "Porte verrouillée (elle le reste au redémarrage)." : "Porte déverrouillée.", "info" );
_ = SaveMapLock( player, door.Key, locked );
}
async Task SaveMapLock( RoleplayPlayer staff, string key, bool locked )
{
var by = $"{staff.RpName} ({staff.SteamId})";
if ( Backend.IsOnline )
{
var response = await Backend.SetMapLock( key, locked, by );
if ( response?.Ok != true && staff.IsValid() )
staff.Notify( "Verrou posé en jeu, mais non enregistré (API injoignable) : il sautera au redémarrage.", "erreur" );
return;
}
var keys = FileSystem.Data.ReadJsonOrDefault<List<string>>( MapLocksFile ) ?? new();
keys.Remove( key );
if ( locked )
keys.Add( key );
FileSystem.Data.WriteJson( MapLocksFile, keys );
}
/// <summary>Hôte, chaque seconde : une fois les portes de la map construites, leurs verrous reposés.</summary>
void TickMapLocks()
{
if ( mapLocksLoaded || sinceMapLocksTry < 30f )
return;
var doors = Scene.GetAllComponents<MapDoor>().Where( x => x.Key is not null ).ToList();
if ( doors.Count == 0 )
return;
mapLocksLoaded = true;
sinceMapLocksTry = 0;
_ = LoadMapLocks( doors );
}
async Task LoadMapLocks( List<MapDoor> doors )
{
List<string> keys;
if ( Backend.IsOnline )
{
keys = (await Backend.LoadMapLocks())?.Keys;
if ( keys is null )
{
// Réessayé 30 s plus tard : sans cela, les portes resteraient ouvertes jusqu'au prochain
// redémarrage.
mapLocksLoaded = false;
return;
}
}
else
keys = FileSystem.Data.ReadJsonOrDefault<List<string>>( MapLocksFile ) ?? new();
var byKey = doors.GroupBy( x => x.Key ).ToDictionary( x => x.Key, x => x.First() );
var found = 0;
foreach ( var key in keys )
{
if ( !byKey.TryGetValue( key, out var door ) || LockedMapDoors.Contains( door.Index ) )
continue;
MapDoors.Remove( door.Index );
LockedMapDoors.Add( door.Index );
found++;
}
if ( keys.Count > 0 )
Log.Info( $"[Horizon] {found} porte(s) de la map verrouillée(s) sur {keys.Count} en base" );
}
}