added Character Setup (#511)
* added Character Setup * whoops * reverted unrelated changes * Made everything work post-rebase * Removed unused PreferencesChanged event * nope, don't need this * HumanoidProfileEditorPanel -> HumanoidProfileEditor * Set initial data for hair pickers * Fixed nullable warning * Renamed LooksComponent -> HumanoidAppearanceComponent * Renamed LooksComponentState -> HumanoidAppearanceComponentState * Final renaming maybe * Use a human-like dummy instead of a real human * Change preferences structs back to classes
This commit is contained in:
committed by
Pieter-Jan Briers
parent
d03da83fda
commit
a4e369e629
@@ -16,7 +16,7 @@
|
||||
<PackageReference Include="YamlDotNet" Version="6.1.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj"/>
|
||||
<ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj">
|
||||
<Private>false</Private>
|
||||
</ProjectReference>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Content.Server.GameObjects.Components
|
||||
|
||||
private static void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
||||
{
|
||||
if (!obj.Session.AttachedEntity.TryGetComponent(out HairComponent hair))
|
||||
if (!obj.Session.AttachedEntity.TryGetComponent(out HumanoidAppearanceComponent looks))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -42,11 +42,11 @@ namespace Content.Server.GameObjects.Components
|
||||
|
||||
if (msg.IsFacialHair)
|
||||
{
|
||||
hair.FacialHairStyleName = msg.HairName;
|
||||
looks.Appearance = looks.Appearance.WithFacialHairStyleName(msg.HairName);
|
||||
}
|
||||
else
|
||||
{
|
||||
hair.HairStyleName = msg.HairName;
|
||||
looks.Appearance = looks.Appearance.WithHairStyleName(msg.HairName);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -57,11 +57,11 @@ namespace Content.Server.GameObjects.Components
|
||||
|
||||
if (msg.IsFacialHair)
|
||||
{
|
||||
hair.FacialHairColor = color;
|
||||
looks.Appearance = looks.Appearance.WithFacialHairColor(color);
|
||||
}
|
||||
else
|
||||
{
|
||||
hair.HairColor = color;
|
||||
looks.Appearance = looks.Appearance.WithHairColor(color);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -75,7 +75,7 @@ namespace Content.Server.GameObjects.Components
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventArgs.User.TryGetComponent(out HairComponent hair))
|
||||
if (!eventArgs.User.TryGetComponent(out HumanoidAppearanceComponent looks))
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't have any hair!"));
|
||||
return;
|
||||
@@ -83,8 +83,8 @@ namespace Content.Server.GameObjects.Components
|
||||
|
||||
_userInterface.Open(actor.playerSession);
|
||||
|
||||
var msg = new MagicMirrorInitialDataMessage(hair.HairColor, hair.FacialHairColor, hair.HairStyleName,
|
||||
hair.FacialHairStyleName);
|
||||
var msg = new MagicMirrorInitialDataMessage(looks.Appearance.HairColor, looks.Appearance.FacialHairColor, looks.Appearance.HairStyleName,
|
||||
looks.Appearance.FacialHairStyleName);
|
||||
|
||||
_userInterface.SendMessage(msg, actor.playerSession);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.GameObjects;
|
||||
namespace Content.Server.GameObjects.Components.Mobs
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class HairComponent : SharedHairComponent
|
||||
public sealed class HumanoidAppearanceComponent : SharedHumanoidAppearanceComponent
|
||||
{
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects;
|
||||
using Content.Server.GameObjects.Components.Markers;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameTicking.GamePresets;
|
||||
using Content.Server.Interfaces;
|
||||
using Content.Server.Interfaces.Chat;
|
||||
@@ -13,6 +14,7 @@ using Content.Server.Players;
|
||||
using Content.Shared;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Content.Shared.Jobs;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Server.Interfaces.Maps;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Server.Player;
|
||||
@@ -41,16 +43,37 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
public class GameTicker : SharedGameTicker, IGameTicker
|
||||
{
|
||||
private const string PlayerPrototypeName = "HumanMob_Content";
|
||||
private const string ObserverPrototypeName = "MobObserver";
|
||||
private const string MapFile = "Maps/stationstation.yml";
|
||||
|
||||
// Seconds.
|
||||
private const float LobbyDuration = 20;
|
||||
|
||||
[ViewVariables] private readonly List<GameRule> _gameRules = new List<GameRule>();
|
||||
|
||||
// Value is whether they're ready.
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<IPlayerSession, bool> _playersInLobby = new Dictionary<IPlayerSession, bool>();
|
||||
|
||||
[ViewVariables] private bool _initialized;
|
||||
|
||||
[ViewVariables] private Type _presetType;
|
||||
|
||||
[ViewVariables] private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers;
|
||||
private DateTime _roundStartTimeUtc;
|
||||
[ViewVariables] private GameRunLevel _runLevel;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private GridCoordinates _spawnPoint;
|
||||
|
||||
[ViewVariables] private bool LobbyEnabled => _configurationManager.GetCVar<bool>("game.lobbyenabled");
|
||||
|
||||
[ViewVariables]
|
||||
public GameRunLevel RunLevel
|
||||
{
|
||||
get => _runLevel;
|
||||
private set
|
||||
{
|
||||
if (_runLevel == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_runLevel == value) return;
|
||||
|
||||
var old = _runLevel;
|
||||
_runLevel = value;
|
||||
@@ -61,46 +84,6 @@ namespace Content.Server.GameTicking
|
||||
|
||||
public event Action<GameRunLevelChangedEventArgs> OnRunLevelChanged;
|
||||
|
||||
private const string PlayerPrototypeName = "HumanMob_Content";
|
||||
private const string ObserverPrototypeName = "MobObserver";
|
||||
private const string MapFile = "Maps/stationstation.yml";
|
||||
|
||||
// Seconds.
|
||||
private const float LobbyDuration = 20;
|
||||
|
||||
[ViewVariables] private bool _initialized;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private GridCoordinates _spawnPoint;
|
||||
[ViewVariables] private GameRunLevel _runLevel;
|
||||
|
||||
[ViewVariables] private bool LobbyEnabled => _configurationManager.GetCVar<bool>("game.lobbyenabled");
|
||||
|
||||
// Value is whether they're ready.
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<IPlayerSession, bool> _playersInLobby = new Dictionary<IPlayerSession, bool>();
|
||||
|
||||
[ViewVariables] private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers;
|
||||
private DateTime _roundStartTimeUtc;
|
||||
|
||||
[ViewVariables] private readonly List<GameRule> _gameRules = new List<GameRule>();
|
||||
|
||||
[ViewVariables] private Type _presetType;
|
||||
|
||||
#pragma warning disable 649
|
||||
[Dependency] private IEntityManager _entityManager;
|
||||
[Dependency] private IMapManager _mapManager;
|
||||
[Dependency] private IMapLoader _mapLoader;
|
||||
[Dependency] private IGameTiming _gameTiming;
|
||||
[Dependency] private IConfigurationManager _configurationManager;
|
||||
[Dependency] private IPlayerManager _playerManager;
|
||||
[Dependency] private IChatManager _chatManager;
|
||||
[Dependency] private IServerNetManager _netManager;
|
||||
[Dependency] private IDynamicTypeFactory _dynamicTypeFactory;
|
||||
[Dependency] private IPrototypeManager _prototypeManager;
|
||||
[Dependency] private readonly ILocalizationManager _localization;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom;
|
||||
[Dependency] private readonly IServerPreferencesManager _prefsManager;
|
||||
#pragma warning restore 649
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
DebugTools.Assert(!_initialized);
|
||||
@@ -122,9 +105,7 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
if (RunLevel != GameRunLevel.PreRoundLobby || _roundStartTimeUtc > DateTime.UtcNow ||
|
||||
_roundStartCountdownHasNotStartedYetDueToNoPlayers)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StartRound();
|
||||
}
|
||||
@@ -144,13 +125,9 @@ namespace Content.Server.GameTicking
|
||||
else
|
||||
{
|
||||
if (_playerManager.PlayerCount == 0)
|
||||
{
|
||||
_roundStartCountdownHasNotStartedYetDueToNoPlayers = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_roundStartTimeUtc = DateTime.UtcNow + TimeSpan.FromSeconds(LobbyDuration);
|
||||
}
|
||||
|
||||
_sendStatusToAll();
|
||||
}
|
||||
@@ -168,10 +145,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
foreach (var (playerSession, ready) in _playersInLobby.ToList())
|
||||
{
|
||||
if (LobbyEnabled && !ready)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (LobbyEnabled && !ready) continue;
|
||||
|
||||
_spawnPlayer(playerSession);
|
||||
}
|
||||
@@ -192,41 +166,28 @@ namespace Content.Server.GameTicking
|
||||
targetPlayer.ContentData().WipeMind();
|
||||
|
||||
if (LobbyEnabled)
|
||||
{
|
||||
_playerJoinLobby(targetPlayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_spawnPlayer(targetPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
public void MakeObserve(IPlayerSession player)
|
||||
{
|
||||
if (!_playersInLobby.ContainsKey(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!_playersInLobby.ContainsKey(player)) return;
|
||||
|
||||
_spawnObserver(player);
|
||||
}
|
||||
|
||||
public void MakeJoinGame(IPlayerSession player)
|
||||
{
|
||||
if (!_playersInLobby.ContainsKey(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!_playersInLobby.ContainsKey(player)) return;
|
||||
|
||||
_spawnPlayer(player);
|
||||
}
|
||||
|
||||
public void ToggleReady(IPlayerSession player, bool ready)
|
||||
{
|
||||
if (!_playersInLobby.ContainsKey(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!_playersInLobby.ContainsKey(player)) return;
|
||||
|
||||
_playersInLobby[player] = ready;
|
||||
_netManager.ServerSendMessage(_getStatusMsg(player), player.ConnectedClient);
|
||||
@@ -244,10 +205,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
public void RemoveGameRule(GameRule rule)
|
||||
{
|
||||
if (_gameRules.Contains(rule))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_gameRules.Contains(rule)) return;
|
||||
|
||||
rule.Removed();
|
||||
|
||||
@@ -258,10 +216,8 @@ namespace Content.Server.GameTicking
|
||||
|
||||
public void SetStartPreset(Type type)
|
||||
{
|
||||
if (!typeof(GamePreset).IsAssignableFrom(type))
|
||||
{
|
||||
throw new ArgumentException("type must inherit GamePreset");
|
||||
}
|
||||
if (!typeof(GamePreset).IsAssignableFrom(type)) throw new ArgumentException("type must inherit GamePreset");
|
||||
|
||||
_presetType = type;
|
||||
UpdateInfoText();
|
||||
}
|
||||
@@ -280,6 +236,7 @@ namespace Content.Server.GameTicking
|
||||
Logger.Error("{0} is an invalid equipment slot.", slotStr);
|
||||
continue;
|
||||
}
|
||||
|
||||
var equipmentEntity = _entityManager.SpawnEntity(equipmentStr, entity.Transform.GridPosition);
|
||||
inventory.Equip(slot, equipmentEntity.GetComponent<ClothingComponent>());
|
||||
}
|
||||
@@ -288,6 +245,14 @@ namespace Content.Server.GameTicking
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void ApplyCharacterProfile(IEntity entity, ICharacterProfile profile)
|
||||
{
|
||||
if (profile is null)
|
||||
return;
|
||||
entity.GetComponent<HumanoidAppearanceComponent>().UpdateFromProfile(profile);
|
||||
entity.Name = profile.Name;
|
||||
}
|
||||
|
||||
private IEntity _spawnObserverMob()
|
||||
{
|
||||
return _entityManager.SpawnEntityAt(ObserverPrototypeName, _getLateJoinSpawnPoint());
|
||||
@@ -301,16 +266,10 @@ namespace Content.Server.GameTicking
|
||||
foreach (var entity in _entityManager.GetEntities(new TypeEntityQuery(typeof(SpawnPointComponent))))
|
||||
{
|
||||
var point = entity.GetComponent<SpawnPointComponent>();
|
||||
if (point.SpawnType == SpawnPointType.LateJoin)
|
||||
{
|
||||
possiblePoints.Add(entity.Transform.GridPosition);
|
||||
}
|
||||
if (point.SpawnType == SpawnPointType.LateJoin) possiblePoints.Add(entity.Transform.GridPosition);
|
||||
}
|
||||
|
||||
if (possiblePoints.Count != 0)
|
||||
{
|
||||
location = _robustRandom.Pick(possiblePoints);
|
||||
}
|
||||
if (possiblePoints.Count != 0) location = _robustRandom.Pick(possiblePoints);
|
||||
|
||||
return location;
|
||||
}
|
||||
@@ -323,44 +282,29 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
// Delete all entities.
|
||||
foreach (var entity in _entityManager.GetEntities().ToList())
|
||||
{
|
||||
// TODO: Maybe something less naive here?
|
||||
// FIXME: Actually, definitely.
|
||||
entity.Delete();
|
||||
}
|
||||
|
||||
// Delete all maps outside of nullspace.
|
||||
foreach (var mapId in _mapManager.GetAllMapIds().ToList())
|
||||
{
|
||||
// TODO: Maybe something less naive here?
|
||||
if (mapId != MapId.Nullspace)
|
||||
{
|
||||
_mapManager.DeleteMap(mapId);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the minds of everybody.
|
||||
// TODO: Maybe move this into a separate manager?
|
||||
foreach (var unCastData in _playerManager.GetAllPlayerData())
|
||||
{
|
||||
unCastData.ContentData().WipeMind();
|
||||
}
|
||||
foreach (var unCastData in _playerManager.GetAllPlayerData()) unCastData.ContentData().WipeMind();
|
||||
|
||||
// Clear up any game rules.
|
||||
foreach (var rule in _gameRules)
|
||||
{
|
||||
rule.Removed();
|
||||
}
|
||||
foreach (var rule in _gameRules) rule.Removed();
|
||||
|
||||
_gameRules.Clear();
|
||||
|
||||
// Move everybody currently in the server to lobby.
|
||||
foreach (var player in _playerManager.GetAllPlayers())
|
||||
{
|
||||
if (_playersInLobby.ContainsKey(player))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_playersInLobby.ContainsKey(player)) continue;
|
||||
|
||||
_playerJoinLobby(player);
|
||||
}
|
||||
@@ -388,9 +332,7 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
// Always make sure the client has player data. Mind gets assigned on spawn.
|
||||
if (session.Data.ContentDataUncast == null)
|
||||
{
|
||||
session.Data.ContentDataUncast = new PlayerData(session.SessionId);
|
||||
}
|
||||
|
||||
// timer time must be > tick length
|
||||
Timer.Spawn(0, args.Session.JoinGame);
|
||||
@@ -437,10 +379,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
case SessionStatus.Disconnected:
|
||||
{
|
||||
if (_playersInLobby.ContainsKey(session))
|
||||
{
|
||||
_playersInLobby.Remove(session);
|
||||
}
|
||||
if (_playersInLobby.ContainsKey(session)) _playersInLobby.Remove(session);
|
||||
|
||||
_chatManager.DispatchServerAnnouncement($"Player {args.Session.SessionId} left server!");
|
||||
break;
|
||||
@@ -460,6 +399,10 @@ namespace Content.Server.GameTicking
|
||||
|
||||
var mob = _spawnPlayerMob(job);
|
||||
data.Mind.TransferTo(mob);
|
||||
var character = _prefsManager
|
||||
.GetPreferences(session.SessionId.Username)
|
||||
.SelectedCharacter;
|
||||
ApplyCharacterProfile(mob, character);
|
||||
}
|
||||
|
||||
private void _spawnObserver(IPlayerSession session)
|
||||
@@ -485,11 +428,9 @@ namespace Content.Server.GameTicking
|
||||
|
||||
private void _playerJoinGame(IPlayerSession session)
|
||||
{
|
||||
_chatManager.DispatchServerMessage(session, $"Welcome to Space Station 14! If this is your first time checking out the game, be sure to check out the tutorial in the top left!");
|
||||
if (_playersInLobby.ContainsKey(session))
|
||||
{
|
||||
_playersInLobby.Remove(session);
|
||||
}
|
||||
_chatManager.DispatchServerMessage(session,
|
||||
"Welcome to Space Station 14! If this is your first time checking out the game, be sure to check out the tutorial in the top left!");
|
||||
if (_playersInLobby.ContainsKey(session)) _playersInLobby.Remove(session);
|
||||
|
||||
_netManager.ServerSendMessage(_netManager.CreateNetMessage<MsgTickerJoinGame>(), session.ConnectedClient);
|
||||
}
|
||||
@@ -514,9 +455,7 @@ namespace Content.Server.GameTicking
|
||||
private void _sendStatusToAll()
|
||||
{
|
||||
foreach (var player in _playersInLobby.Keys)
|
||||
{
|
||||
_netManager.ServerSendMessage(_getStatusMsg(player), player.ConnectedClient);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetInfoText()
|
||||
@@ -538,6 +477,22 @@ The current game mode is [color=white]{0}[/color]", gameMode);
|
||||
{
|
||||
return _dynamicTypeFactory.CreateInstance<GamePreset>(_presetType ?? typeof(PresetSandbox));
|
||||
}
|
||||
|
||||
#pragma warning disable 649
|
||||
[Dependency] private IEntityManager _entityManager;
|
||||
[Dependency] private IMapManager _mapManager;
|
||||
[Dependency] private IMapLoader _mapLoader;
|
||||
[Dependency] private IGameTiming _gameTiming;
|
||||
[Dependency] private IConfigurationManager _configurationManager;
|
||||
[Dependency] private IPlayerManager _playerManager;
|
||||
[Dependency] private IChatManager _chatManager;
|
||||
[Dependency] private IServerNetManager _netManager;
|
||||
[Dependency] private IDynamicTypeFactory _dynamicTypeFactory;
|
||||
[Dependency] private IPrototypeManager _prototypeManager;
|
||||
[Dependency] private readonly ILocalizationManager _localization;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom;
|
||||
[Dependency] private readonly IServerPreferencesManager _prefsManager;
|
||||
#pragma warning restore 649
|
||||
}
|
||||
|
||||
public enum GameRunLevel
|
||||
@@ -549,13 +504,13 @@ The current game mode is [color=white]{0}[/color]", gameMode);
|
||||
|
||||
public class GameRunLevelChangedEventArgs : EventArgs
|
||||
{
|
||||
public GameRunLevel OldRunLevel { get; }
|
||||
public GameRunLevel NewRunLevel { get; }
|
||||
|
||||
public GameRunLevelChangedEventArgs(GameRunLevel oldRunLevel, GameRunLevel newRunLevel)
|
||||
{
|
||||
OldRunLevel = oldRunLevel;
|
||||
NewRunLevel = newRunLevel;
|
||||
}
|
||||
|
||||
public GameRunLevel OldRunLevel { get; }
|
||||
public GameRunLevel NewRunLevel { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,27 +29,28 @@ namespace Content.Server.Preferences
|
||||
|
||||
var profiles = new ICharacterProfile[_maxCharacterSlots];
|
||||
foreach (var profile in prefs.HumanoidProfiles)
|
||||
profiles[profile.Slot] = new HumanoidCharacterProfile
|
||||
{
|
||||
Name = profile.CharacterName,
|
||||
Age = profile.Age,
|
||||
Sex = profile.Sex == "Male" ? Male : Female,
|
||||
CharacterAppearance = new HumanoidCharacterAppearance
|
||||
{
|
||||
HairStyleName = profile.HairName,
|
||||
HairColor = Color.FromHex(profile.HairColor),
|
||||
FacialHairStyleName = profile.FacialHairName,
|
||||
FacialHairColor = Color.FromHex(profile.FacialHairColor),
|
||||
EyeColor = Color.FromHex(profile.EyeColor),
|
||||
SkinColor = Color.FromHex(profile.SkinColor)
|
||||
}
|
||||
};
|
||||
{
|
||||
profiles[profile.Slot] = new HumanoidCharacterProfile(
|
||||
profile.CharacterName,
|
||||
profile.Age,
|
||||
profile.Sex == "Male" ? Male : Female,
|
||||
new HumanoidCharacterAppearance
|
||||
(
|
||||
profile.HairName,
|
||||
Color.FromHex(profile.HairColor),
|
||||
profile.FacialHairName,
|
||||
Color.FromHex(profile.FacialHairColor),
|
||||
Color.FromHex(profile.EyeColor),
|
||||
Color.FromHex(profile.SkinColor)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new PlayerPreferences
|
||||
{
|
||||
SelectedCharacterIndex = prefs.SelectedCharacterSlot,
|
||||
Characters = profiles.ToList()
|
||||
};
|
||||
(
|
||||
profiles,
|
||||
prefs.SelectedCharacterSlot
|
||||
);
|
||||
}
|
||||
|
||||
public void SaveSelectedCharacterIndex(string username, int index)
|
||||
@@ -71,10 +72,10 @@ namespace Content.Server.Preferences
|
||||
if (!(profile is HumanoidCharacterProfile humanoid))
|
||||
// TODO: Handle other ICharacterProfile implementations properly
|
||||
throw new NotImplementedException();
|
||||
|
||||
var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
|
||||
_prefsDb.SaveCharacterSlot(username, new HumanoidProfile
|
||||
{
|
||||
SlotName = humanoid.Name,
|
||||
CharacterName = humanoid.Name,
|
||||
Age = humanoid.Age,
|
||||
Sex = humanoid.Sex.ToString(),
|
||||
|
||||
Reference in New Issue
Block a user