Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/locate command #150

Merged
merged 17 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
71 changes: 71 additions & 0 deletions CelesteNet.Client/Components/CelesteNetChatComponent.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using Celeste.Mod.CelesteNet.Client.Entities;
using Celeste.Mod.CelesteNet.DataTypes;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Monocle;
using static Celeste.Mod.CelesteNet.Client.Components.CelesteNetPlayerListComponent;

namespace Celeste.Mod.CelesteNet.Client.Components {
public class CelesteNetChatComponent : CelesteNetGameComponent {
Expand Down Expand Up @@ -349,9 +353,76 @@ public void Handle(CelesteNetConnection con, DataChannelList channelList) {
CurrentChannelName = tmp;
}

#nullable enable
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.";

DataPlayerState? state = null;
if (Client?.Data?.TryGetBoundRef(target, out state) == false || state == null) return;

// Abuse the player list representation to get a formatted version
var fakeBlob = new BlobPlayer();
Context.Get<CelesteNetPlayerListComponent>().GetState(fakeBlob, state);
var location = fakeBlob.Location;

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

string iconEmoji = "";

if (location.Icon != null) {
string emojiId = $"celestenet_SID_{location.SID}_";
if (!Emoji.Registered.Contains(emojiId)) {
// We need to downscale the icon to fit in chat
MTexture? icon =
(fakeBlob.LocationMode & LocationModes.Icons) != 0 && GFX.Gui.Has(location.Icon)
? GFX.Gui[location.Icon]
: null;
if (icon != null) {
float scale = 64f / icon.Height;

var tex = new MTexture(new MTexture(icon.Texture), icon.ClipRect) { ScaleFix = scale };
Emoji.Register(emojiId, tex);
Emoji.Fill(CelesteNetClientFont.Font);
}
}
if (Emoji.Registered.Contains(emojiId))
iconEmoji = $":{emojiId}:";
}

string locationName = location.Name;

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

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

chat.Text = $"{target.FullName} is in room '{location.Level}' of {locationName}.";
}
#nullable restore

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
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ 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;
Expand Down
52 changes: 52 additions & 0 deletions CelesteNet.Server.ChatModule/CMDs/CmdLocate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using Celeste.Mod.CelesteNet.DataTypes;
using MonoMod.Utils;

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.";

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",
Text = "If you see this, either your client does not properly handle /locate or this is a bug. Please report!",
Color = Chat.Settings.ColorCommandReply
};
self.Con.Send(Chat.PrepareAndLog(self, chat));
}

}