Skip to content

Commit

Permalink
Merge pull request #150 from balt-dev/main
Browse files Browse the repository at this point in the history
/locate command
  • Loading branch information
RedFlames authored Aug 18, 2024
2 parents 27ac4d7 + 6c5611b commit 567bb00
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 29 deletions.
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

0 comments on commit 567bb00

Please sign in to comment.