Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CelesteNet.Client/CelesteNetClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public CelesteNetClient(CelesteNetClientSettings settings, CelesteNetClientOptio
Options.ClientID = Settings.ClientID;
Options.InstanceID = Settings.InstanceID;

Options.SupportedClientFeatures |= CelesteNetSupportedClientFeatures.LocateCommand;

bool isServerLocalhost = Settings.Host == "localhost" || Settings.Host == "127.0.0.1";

if (Settings.ClientIDSending == cIDSendMode.Off || (Settings.ClientIDSending == cIDSendMode.NotOnLocalhost && isServerLocalhost))
Expand Down
74 changes: 74 additions & 0 deletions CelesteNet.Client/CelesteNetLocationInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Celeste.Mod.CelesteNet.DataTypes;

namespace Celeste.Mod.CelesteNet.Client {
public class CelesteNetLocationInfo {

public string SID { get; set; }
public AreaData Area { get; set; }
public string Name { get; set; }
public string Side { get; set; }
public string Level { get; set; }
public string Icon { get; set; }
public string EmoteID => string.IsNullOrWhiteSpace(SID) ? "" : $"celestenet_SID_{SID}_";

private bool emoteLoaded = false;
public string Emote => LoadIconEmote() ? $":{EmoteID}:" : "";
public bool IsRandomizer => Name.StartsWith("randomizer/");

public CelesteNetLocationInfo() { }

public CelesteNetLocationInfo(string sid) {
SID = sid;
Area = AreaData.Get(SID);

Name = Area?.Name?.DialogCleanOrNull(Dialog.Languages["english"]) ?? SID;
Side = "A";
Level = "";
Icon = "";

if (!IsRandomizer && Area != null) {
Icon = Area.Icon;

string lobbySID = Area?.Meta?.Parent;
AreaData lobby = string.IsNullOrEmpty(lobbySID) ? null : AreaData.Get(lobbySID);
if (lobby?.Icon != null)
Icon = lobby.Icon;
}
}

public CelesteNetLocationInfo(DataPlayerState state) : this(state?.SID) {

if (state != null) {
Side = ((char)('A' + (int)state.Mode)).ToString();
Level = state.Level;
}
}

public bool LoadIconEmote() {
if (emoteLoaded)
return true;

if (
// Icon exists
!string.IsNullOrWhiteSpace(Icon) &&
// Can construct Emoji ID
!string.IsNullOrWhiteSpace(EmoteID) &&
// Icon is loaded
GFX.Gui.Has(Icon)
) {
if (!Emoji.Registered.Contains(EmoteID)) {
// We need to downscale the icon to fit in chat
// Due to our previous checks, this is never null
Monocle.MTexture icon = GFX.Gui[Icon];
float scale = 64f / icon.Height;

Monocle.MTexture tex = new(new(icon.Texture), icon.ClipRect) { ScaleFix = scale };
Emoji.Register(EmoteID, tex);
Emoji.Fill(CelesteNetClientFont.Font);
}
emoteLoaded = Emoji.Registered.Contains(EmoteID);
}
return emoteLoaded;
}
}
}
52 changes: 50 additions & 2 deletions CelesteNet.Client/Components/CelesteNetChatComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,55 @@ public void Handle(CelesteNetConnection con, DataChannelList channelList) {
CurrentChannelName = tmp;
}

public void HandleLocate(DataChat chat) {
// For some reason, using the actual target field always came up as null for me.
// Instead, I'm opting to use the Player field, Because It Works :TM:
var target = chat.Player;

chat.Player = null;
chat.Tag = "";

if (target == null) {
chat.Text = $"Target of /locate not found.";
return;
}

chat.Text = $"{target.FullName} isn't in game or is in another channel.";

// This has to be set, or we will return early
DataPlayerState state = null;
if (Client?.Data?.TryGetBoundRef(target, out state) == false || state == null) return;

if (string.IsNullOrWhiteSpace(state.SID)) {
// We fall back to the above assignment
return;
}

CelesteNetLocationInfo location = new(state);

string locationTitle = location.Name;
string iconEmoji = "";

if ((Settings.PlayerListUI.ShowPlayerListLocations & CelesteNetPlayerListComponent.LocationModes.Icons) != 0) {
iconEmoji = location.Emote;
}

if (!string.IsNullOrEmpty(iconEmoji))
locationTitle = $"{iconEmoji} {locationTitle}";

if (!string.IsNullOrEmpty(location.Side))
locationTitle += $" {location.Side}";

chat.Text = $"{target.FullName} is in room '{location.Level}' of {locationTitle}.";
}

public void Handle(CelesteNetConnection con, DataChat msg) {
if (Client == null)
return;

if (msg.Tag == "locate") {
HandleLocate(msg);
}

if (Settings.PlayerListUI.HideOwnChannelName) {
// don't get too eager, only replace text in ACK'd commands and server responses
Expand Down Expand Up @@ -556,7 +602,9 @@ public override void Update(GameTime gameTime) {
// completions for commands or their first parameter
if (Typing.StartsWith("/")) {
int firstSpace = Typing.IndexOf(" ");
CommandInfo cmd = firstSpace == -1 ? null : CommandList.FirstOrDefault(c => c.ID == Typing.Substring(1, firstSpace - 1));
CommandInfo cmd = firstSpace == -1 ? null : CommandList.FirstOrDefault(cmd =>
cmd.ID == Typing.Substring(1, firstSpace - 1)
);

if (Typing.Substring(0, _CursorIndex).Equals(completable)) {
UpdateCompletion(CompletionType.Command, completable.Substring(1).ToLowerInvariant());
Expand Down Expand Up @@ -757,7 +805,7 @@ public void UpdateCompletion(CompletionType type, string partial = "") {
break;

case CompletionType.Emoji:
IEnumerable<string> filter_emotes = Emoji.Registered.Where(name => !name.StartsWith("celestenet_avatar_"));
IEnumerable<string> filter_emotes = Emoji.Registered.Where(name => !name.StartsWith("celestenet_avatar_") && !name.StartsWith("celestenet_SID_"));

if (string.IsNullOrEmpty(partial)) {
Completion = filter_emotes.ToList();
Expand Down
31 changes: 10 additions & 21 deletions CelesteNet.Client/Components/CelesteNetPlayerListComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -528,32 +528,21 @@ private DataPlayerInfo ListPlayerUnderChannel(BlobPlayer blob, DataPlayerInfo pl
}
}

private void GetState(BlobPlayer blob, DataPlayerState state) {
public void GetState(BlobPlayer blob, DataPlayerState state) {
if (!string.IsNullOrWhiteSpace(state.SID)) {
AreaData area = AreaData.Get(state.SID);
string chapter = area?.Name?.DialogCleanOrNull(Dialog.Languages["english"]) ?? state.SID;
CelesteNetLocationInfo location = new(state);

blob.Location.Color = DefaultLevelColor;
blob.Location.TitleColor = Color.Lerp(area?.TitleBaseColor ?? Color.White, DefaultLevelColor, 0.5f);
blob.Location.AccentColor = Color.Lerp(area?.TitleAccentColor ?? Color.White, DefaultLevelColor, 0.8f);
blob.Location.TitleColor = Color.Lerp(location.Area?.TitleBaseColor ?? Color.White, DefaultLevelColor, 0.5f);
blob.Location.AccentColor = Color.Lerp(location.Area?.TitleAccentColor ?? Color.White, DefaultLevelColor, 0.8f);

blob.Location.SID = state.SID;
blob.Location.Name = chapter;
blob.Location.Side = ((char) ('A' + (int) state.Mode)).ToString();
blob.Location.Level = state.Level;
blob.Location.SID = location.SID;
blob.Location.Name = location.Name;
blob.Location.Side = location.Side;
blob.Location.Level = location.Level;

blob.Location.IsRandomizer = chapter.StartsWith("randomizer/");

if (blob.Location.IsRandomizer || area == null) {
blob.Location.Icon = "";
} else {
blob.Location.Icon = area?.Icon ?? "";

string lobbySID = area?.Meta?.Parent;
AreaData lobby = string.IsNullOrEmpty(lobbySID) ? null : AreaData.Get(lobbySID);
if (lobby?.Icon != null)
blob.Location.Icon = lobby.Icon;
}
blob.Location.IsRandomizer = location.IsRandomizer;
blob.Location.Icon = location.Icon;

ShortenRandomizerLocation(ref blob.Location);
}
Expand Down
8 changes: 7 additions & 1 deletion CelesteNet.Client/Handshake.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public static class Handshake {
");

foreach (FieldInfo field in typeof(CelesteNetClientOptions).GetFields(BindingFlags.Public | BindingFlags.Instance)) {
object optionValue = field.GetValue(options);

if (field.FieldType.IsEnum) {
optionValue = Convert.ChangeType(optionValue, Enum.GetUnderlyingType(field.FieldType));
}

switch (Type.GetTypeCode(field.FieldType)) {
case TypeCode.Boolean:
case TypeCode.Int16:
Expand All @@ -38,7 +44,7 @@ public static class Handshake {
case TypeCode.UInt64:
case TypeCode.Single:
case TypeCode.Double: {
reqBuilder.AppendLine($"CelesteNet-ClientOptions-{field.Name}: {field.GetValue(options)}");
reqBuilder.AppendLine($"CelesteNet-ClientOptions-{field.Name}: {optionValue}");
} break;
}
}
Expand Down
55 changes: 55 additions & 0 deletions CelesteNet.Server.ChatModule/CMDs/CmdLocate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using Celeste.Mod.CelesteNet.DataTypes;

namespace Celeste.Mod.CelesteNet.Server.Chat.Cmd;

public class CmdLocate : ChatCmd {

public override CompletionType Completion => CompletionType.Player;

public override string Info => "Find where a player is.";

public override CelesteNetSupportedClientFeatures RequiredFeatures =>
CelesteNetSupportedClientFeatures.LocateCommand;

private CelesteNetPlayerSession? Other;
private DataPlayerInfo? OtherPlayer;

public override void Init(ChatModule chat) {
Chat = chat;

ArgParser parser = new(chat, this);
parser.AddParameter(new ParamPlayerSession(chat, ValidatePlayerSession));
ArgParsers.Add(parser);
}

private void ValidatePlayerSession(string raw, CmdEnv env, ICmdArg arg) {
if (arg is not CmdArgPlayerSession sessionArg)
throw new CommandRunException("Invalid username or ID.");

CelesteNetPlayerSession? other = sessionArg.Session;
DataPlayerInfo otherPlayer = other?.PlayerInfo ?? throw new CommandRunException("Invalid username or ID.");

Other = other;
OtherPlayer = otherPlayer;
}


public override void Run(CmdEnv env, List<ICmdArg>? args) {
if (Other == null || OtherPlayer == null)
throw new InvalidOperationException("This should never happen if ValidatePlayerSession returns without error.");

CelesteNetPlayerSession? self = env.Session ?? throw new CommandRunException("Cannot locate as the server.");

var chat = new DataChat {
Player = OtherPlayer,
Tag = "locate",
// On older clients, we never get here. On newer clients, this is replaced.
Text = "{YOU SHOULD NEVER SEE THIS, PLEASE REPORT}",
Color = Chat.Settings.ColorCommandReply
};
self.Con.Send(chat);
}

}
9 changes: 8 additions & 1 deletion CelesteNet.Server.ChatModule/CMDs/CommandsContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public CommandsContext(ChatModule chat) {
Auth = cmd.MustAuth,
AuthExec = cmd.MustAuthExec,
FirstArg = cmd.Completion,
AliasTo = cmd.InternalAliasing ? "" : aliasTo?.ID ?? ""
AliasTo = cmd.InternalAliasing ? "" : aliasTo?.ID ?? "",
RequiredFeatures = cmd.RequiredFeatures
};
}

Expand Down Expand Up @@ -78,6 +79,7 @@ public abstract class ChatCmd {
#pragma warning restore CS8618

public List<ArgParser> ArgParsers = new();
public virtual CelesteNetSupportedClientFeatures RequiredFeatures => CelesteNetSupportedClientFeatures.None;

public virtual string ID => GetType().Name.Substring(3).ToLowerInvariant();

Expand All @@ -104,6 +106,11 @@ public virtual void ParseAndRun(CmdEnv env) {
return;
}

if (env.Session != null && !env.Session.CheckClientFeatureSupport(RequiredFeatures)) {
env.Error(new CommandRunException("Command is unsupported by your client! Try updating CelesteNet."));
return;
}

List<Exception> caught = new();

if (ArgParsers.Count == 0) {
Expand Down
1 change: 1 addition & 0 deletions CelesteNet.Server.ChatModule/ChatModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ private void OnSessionStart(CelesteNetPlayerSession session) {
Broadcast(Settings.MessageGreeting.InjectSingleValue("player", session.PlayerInfo?.FullName ?? "???"));
SendTo(session, Settings.MessageMOTD);
}

session.SendCommandList(Commands.DataAll);

SpamContext spam = session.Set(this, new SpamContext(this));
Expand Down
8 changes: 6 additions & 2 deletions CelesteNet.Server/CelesteNetPlayerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ internal CelesteNetPlayerSession(CelesteNetServer server, CelesteNetConnection c
}

public bool CheckClientFeatureSupport(CelesteNetSupportedClientFeatures features) {
return (ClientOptions.SupportedClientFeatures & features) == features;
return ClientOptions.SupportedClientFeatures.HasFlag(features);
}

public T? Get<T>(object ctx) where T : class {
Expand Down Expand Up @@ -430,7 +430,11 @@ public void SendCommandList(DataCommandList commands) {
authExec = info.Tags.Contains(BasicUserInfo.TAG_AUTH_EXEC);
}

filteredCommands.List = commands.List.Where(cmd => (!cmd.Auth || auth) && (!cmd.AuthExec || authExec)).ToArray();
filteredCommands.List = commands.List.Where(cmd => {
return (!cmd.Auth || auth)
&& (!cmd.AuthExec || authExec)
&& CheckClientFeatureSupport(cmd.RequiredFeatures);
}).ToArray();

Con.Send(filteredCommands);
}
Expand Down
7 changes: 5 additions & 2 deletions CelesteNet.Shared/CelesteNetClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ public class CelesteNetClientOptions {
public bool AvatarsDisabled = false;
public ulong ClientID;
public uint InstanceID;
public CelesteNetSupportedClientFeatures SupportedClientFeatures;
public CelesteNetSupportedClientFeatures SupportedClientFeatures = CelesteNetSupportedClientFeatures.None;
}

[Flags]
public enum CelesteNetSupportedClientFeatures : ulong {}
public enum CelesteNetSupportedClientFeatures : ulong {
None = 0,
LocateCommand = 1 << 0
}
}
4 changes: 4 additions & 0 deletions CelesteNet.Shared/CommandInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ public class CommandInfo {
public bool Auth = false;
public bool AuthExec = false;
public CompletionType FirstArg = CompletionType.None;

// NOTE: This is not sent to the client/server, for legacy compatibility reasons! Be careful.
// - 23 July, 2024
public CelesteNetSupportedClientFeatures RequiredFeatures = CelesteNetSupportedClientFeatures.None;
}

public enum CompletionType : byte {
Expand Down