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

Persistent Client Preferences and Friends of Friends Packets #48

Open
wants to merge 17 commits into
base: master
Choose a base branch
from

Conversation

desukuran
Copy link
Contributor

This implements the client preferences and relevant Friend Network packets. It also refactors how GameInformation is processed and FriendsGameInfo is sent.

Added Inbound Packet FriendsOfFriendsRequest (5)

  • Observations see that this is usually triggered by a non-friend session ID sending game info to you.
  • This sends a list of session ids presumably to identify.
  • We grab all the info on said session id and put them into a list of UserModels and send it to Outbound FriendsOfFriendsInfo

Added Outbound Packet FriendsOfFriendsInfo (136)

  • This packet expects 5 attributes, all lists containing: Friend Session IDs, User IDs, Usernames, Nicknames and a list of their friends.
  • Process will process the list of UserModels sent from Packet 5 and gets all the info needed.

Added Inbound UserPreferences (10) Replacing file Unknown10

  • This packet sends a keyed map of preferences. (id, value)
  • The logic behind this is simply: If the key exists in the packet, disable it. If the key doesn't exist, enable it. Other than 6 per Zelaron and I's observations, enables if it exists.
  • This then triggers a new function in the database to save your preferences.

Modified Outbound ClientPreferences (141)

  • Changed from the hardcoded table to one that has loaded preferences.

Modifications to GameInformation and FriendsGameInfo

  • SendGameInfoToFriends has moved to PFireServer, as it will be called more from different spots.
  • FriendsGameInfo has been changed to now be more dynamic and process a list of users instead of one. This saves having to send multiple packets for multiple users (when you log in, etc).

Modifications to Inbound ConnectionInformation

  • Chat room mode is now officially disabled. You can re-enable it at the top define. Its not a bad idea, but since we are focusing on getting the core FoF stuff done and moving onto more, I disabled it.
  • You are now sent what your friends are playing on log on.

Modifications to Inbound FriendRequestAccept

  • You are both sent your respective games when accepted.

Modifications to PFireDatabase

  • New functions: QueryFriendsOfFriends (Thanks Darcy) and SaveUserPrefs

Modifications to XFireClient

  • UserCameOnline is added with BroadcastSessionStatus, it might seem redundant but it seemed to fix a weird lingering log on and log off issue. More testing would have to be done, but it seemed to fix it.

Modifications to PFireServer

  • New function: RemoveGamingSession which clears your gaming session before you log off. Friends are fine but FoF lingers, that fixes it.
  • SendGameInfoToFriends is moved from FriendsGameInfo with some refactoring changes. This sends your current game to people who missed the initial game info broadcast packets.

Modifications to UserModel

  • Added Preferences into the user. Darcy spoke to possibly in the future to change this, but here it is for now.

Usual MessageFactory and Type changes for new packets implemented.

Database Changes

  • Added Preferences into the User Table in the Database
  • New Migrations to reflect this.

This implements the client preferences and relevant Friend Network packets. It also refactors how GameInformation is processed and FriendsGameInfo is sent.

Added Inbound Packet FriendsOfFriendsRequest (5)
* Observations see that this is usually triggered by a non-friend session ID sending game info to you.
* This sends a list of session ids presumably to identify.
* We grab all the info on said session id and put them into a list of UserModels and send it to Outbound FriendsOfFriendsInfo

Added Outbound Packet FriendsOfFriendsInfo (136)
* This packet expects 5 attributes, all lists containing: Friend Session IDs, User IDs, Usernames, Nicknames and a list of their friends.
* Process will process the list of UserModels sent from Packet 5 and gets all the info needed.

Added Inbound UserPreferences (10) Replacing file Unknown10
* This packet sends a keyed map of preferences. (id, value)
* The logic behind this is simply: If the key exists in the packet, disable it. If the key doesn't exist, enable it. Other than 6 per Zelaron and I's observations, enables if it exists.
* This then triggers a new function in the database to save your preferences.

Modified Outbound ClientPreferences (141)
* Changed from the hardcoded table to one that has loaded preferences.

Modifications to GameInformation and FriendsGameInfo
* SendGameInfoToFriends has moved to PFireServer, as it will be called more from different spots.
* FriendsGameInfo has been changed to now be more dynamic and process a list of users instead of one. This saves having to send multiple packets for multiple users (when you log in, etc).

Modifications to Inbound ConnectionInformation
* Chat room mode is now officially disabled. You can re-enable it at the top define. Its not a bad idea, but since we are focusing on getting the core FoF stuff done and moving onto more, I disabled it.
* You are now sent what your friends are playing on log on.

Modifications to Inbound FriendRequestAccept
* You are both sent your respective games when accepted.

Modifications to PFireDatabase
* New functions: QueryFriendsOfFriends (Thanks Darcy) and SaveUserPrefs

Modifications to XFireClient
* UserCameOnline is added with BroadcastSessionStatus, it might seem redundant but it seemed to fix a weird lingering log on and log off issue. More testing would have to be done, but it seemed to fix it.

Modifications to PFireServer
* New function: RemoveGamingSession which clears your gaming session before you log off. Friends are fine but FoF lingers, that fixes it.
* SendGameInfoToFriends is moved from FriendsGameInfo with some refactoring changes. This sends your current game to people who missed the initial game info broadcast packets.

Modifications to UserModel
* Added Preferences into the user. Darcy spoke to possibly in the future to change this, but here it is for now.

Usual MessageFactory and Type changes for new packets implemented.

Database Changes
* Added Preferences into the User Table in the Database
* New Migrations to reflect this.
Added Inbound Packets: GroupCreate, GroupMemberAdd, GroupMemberRemove, GroupRemove, GroupRename

* All of these use byte attributes, as such had to be included in the list of non-string attributes.
* I won't break down each one, it all pretty much interacting with the UserGroup part of the database where it needs to be.

Added Outbound GroupCreateConfirmation
* This one just tells the client the deed has been done. I thought the client would send the userid right after but I guess the user does that themselves. As such, I put in null checks and skips where need be.

Updated Outbound Groups and GroupsFriends
* Actually added processing to both with functions from the Database.

Modified MessageSerializer
* Since all the group Inbound stuff do numbered attributes, I added them to the list of single byte hack. Just because of how this is structured, we have to do it like this, its ugly but it will have to be for a while.

PFireDatabase
*  Added all the relevant Group query, insert, remove, etc. functions.

Database Changes
* Added UserGroup model into the Database, added the migration as usual.

Added the usual MessageTypes, Factory, etc as usual.
Added Outbound FriendNameChange
* There is a dedicated packet for nickname change, so we are going to use it.
* Sends the new Nickname and UserID involved.

Modified Inbound NicknameChange
* As per xfyre's implementation, it calls the proper FriendNameChange packet instead of the FriendsList packet.
* Proper Logged in else where packet.

Added Outbound LoggedInElseWhere
* Discovered that reason has other uses, mostly for server maintenance or server upgrade.
* Previously unknown minrect and maxrect nomenclature function has been found. Its the seconds till autologin for the client.

Added it to when theres a duplicate session found and sent.

XFireMessageType updated as usual.
Added Inbound Packets FavoriteServerAdd and FavoriteServerRemove
* These are packets sent by the client for adding and removing favorites. It sends a gameid, ip and port.

Modified Outbound Packet ServerList
* Updated the nomenclature to GameServerSendFavorites
* Maximum from a pre-shutdown packet is 0x1e. As such, I applied that logic to adding servers.

Added new functions to the PFireDatabase GetAllFavoriteServers, AddUserFavoriteServer, RemoveUserFavoriteServer

Usual Database Migrations added.

Added usual MessageTypes and Factory additions.
* This is just a way to notify the user about their Friend Request. Reasons were extracted via decomp checking.

* Added an OK signal on FriendRequest. I think FriendRequest should get refactored at some point looking at it, especially with these responses.

Response
* 0 = OK
* 1 = No such user found.
* 2 = You are already friends with them.
* 3 = Your invitation message for %s has been updated.
* 4 = You have too many invitations and friends.  %s was not invited to be your friend.
* 5 = %s has too many invitations and friends.  They were not invited to be your friend.
* 6 = Your invitation to add %s as a friend was unable to be sent due to a system error.
* Added both Inbound and Outbound packets for letting people know your voice chat status has changed.

* I cannot test this because I don't know how to actually make this activate, but judging by the decomp functions and strings, this should work.
Added Inbound GameClientData
* Grabs the string given by the SDK output.
* Send it to the friends.
* TODO: Should be kept in a database.

Added Outbound FriendGameClientData
* This is a list of sids and client data. Very similar to how Friend of Friends info stuff does it. Probably so you're not pelting out packets.

Added Inbound Packet to the attribute hack, GameClientData uses 0x5b

Added the usual Factory and Types to Packets.
Added Outbound Packet UserScreenshots
* This would give you a list of screenshots given the metadata sent.
* Major thanks to Zelaron just plugging away at it until we cracked the code

Added Screenshot Model
* Written by Zelaron, this Model holds all the relevant data for a screenshot.

* Per usual, this is a frontened-orientated packet, so it has no runtime functionality, but is ready to accept and send data.

Usual MessageType added.
Usually I like to be verbose with my commits and explain, However given the size and scope, I am going to be brief on this one. If you want technical information, please refer to the code. A protocol bible will be written in time. There are some unknowns at the moment, but in time all secrets will be uncovered.

XMessageField has been modified to not include Encoing as it caused lots of issues for non-textual names of attributes especially with chatroom packets. So far, no issues have arised from this and perhaps later there can be a superior fix. But for now, this works.

Added Inbound Chatroom Packet (25)
* This is an all emcompassing chatroom packet. All actions go through this, function is dictated by int CliMsg.
* Added numerous functions based on context of CliMsg. This is all NON-VOICE packets I could implement, including Game Lobbies.
* Game lobbies needs to be tested further, the signed int IP bug might affect connectivity. This will be addressed in time.

Added numerous Outbound Packets (350~389)
* Former Outbound Chatrooms has been renamed to ChatroomListOnLogin. It also fixed the attributes to be DID and not int.
* This is all the ones I could identify. They should work as it should.
* Voice packets are implemented on how I saw them, but they offer no functionality at the moment as nothing calls them.
* For the most part, I tried to keep all the processing in Packet 25 to keep it in one spot. Theres a few exceptions, most notabily the JoinInfo Packet (351) as it deals with room passwords and such.

Database changes
* Added ChatroomEnt to handle all the needed chatroom things. Added ChatroomModel to facilitate queries.
* As such I added numerous functions to query and update various chatroom things and settings.

Ethos on Chatrooms
* Xfire's original logic on the chat list on login were chat rooms you specficially joined in the past. I have changed it to be all chat rooms for now. I believe this also includes friends-only chatrooms. In a later update, I will change the logic.
* Xfire had an original limit of 5 chatrooms per user. There is no limit programmed.
* Xfire's save room function was to save the chatroom to your list (so that you get it everytime you login). I have changed this to save the chatroom even if everyone is left, if its not saved, then the chatroom is deleted, think IRC with no ChanServ.
* If you want to make a room with a specific name, just type it in and it should create if it doesn't already exist.
* If you hit Create Chatroom it will make a chatroom with your name, I put in logic that will check to see if it exists, if so it just adds a _ at the end of the chatname. After you can rename your chatroom.
…74 and 182)

Plus bonus User Videos Outbound Packet (179)

* As per MacFire project protocol handbook, when 37 is called, the server will send back 172, 174, 182 and 176.
* 176 is pending per clan update.

Added Inbound Packet 37 - Request User Advanced Info
* This will ask the server for a user's avatar, videos, screenshots and clan info. Pretty straight forward.

Added Outbound Packet 172 - Friend Screenshot Info
* Self explainatory. Sends friend's uploaded screenshots.

Added Outbound Packet 174 - Friend Avatar Info
* Sends friend's avatar info.
* It must have evolved to include different ways to have an avatar.
* Per Xfyre, avatarType: 0 - Xfire Default Logo
* avatarType: 1 "http://media.xfire.com/xfire/xf/images/avatars/gallery/default/%03u.gif" // avatar id
* avatarType: 2 "http://screenshot.xfire.com/avatar/100/%s.jpg?%u" // username, revision number

Added Outbound Packet 182 - Friend Video Info
* Self explainatory. Sends friend's uploaded videos.

As a bonus, Added Outbound Packet 179 - User Video Info
* Self explainatory. Sends your uploaded videos.

Typical changes to Serializer and MessageTypeFactory to make it all work.
Usually I like to be verbose with my commits and explain, However much like the chatroom packets, given the size and scope, I am going to be brief on this one. If you want technical information, please refer to the code. A protocol bible will be written in time. There are some unknowns at the moment, but in time all secrets will be uncovered.

Added Inbound Accept and Reject Clan Requests Packet (44 and 45)
* These inbound packets are used to process adding the user to a clan, or rejecting the requests.
* Database work needs to be done, but the information is documented.

Added numerous Outbound Packets (158~189)
* Refer to each packet for documentation of function and attributes.
* UserClans should probably be sent on login.
* Attributes should be put in the constructor given. I will do a Model in time, once I figure out the structure.

Unknown Effects
* Ranks, Rank Permissions and Preferences I couldn't figure out functionality as I got no visual feedback. I am sure they are used somewhere but the xfire client gave me nothing. Few bits of legacy screenshots show that ranks aren't shown. So I have no clue. I wonder if it has to do with chatrooms, but I am not sure.
var LaunchStatus = (int)MsgList[0x5A];

var UserPowerLevel = await context.Server.Database.GetUserHighestChatPowerLevel(Cid, context.User.Id);
if (UserPowerLevel > 3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make all these numbers a constant or enum or something? What does "3" mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll toss in an enum when I get here.

var requestId = (int)MsgList[0x0B];
var newName = (string)MsgList[0x05];

if (context.Server.Database.QueryChatroom(newName) == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing await here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

Copy link
Owner

@darcymiranda darcymiranda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still reviewing; posting what I have so far.

.Where(session => session != null)
.ToList();

if (sessions.Count != 0)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is not needed

[XMessageField("fnsid")]
public List<Guid> Fnsid { get; } = new List<Guid>();
[XMessageField("userid")]
public List<int> Userid { get; } = new List<int>();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public List<int> Userid { get; } = new List<int>();
public List<int> UserId { get; } = new List<int>();

foreach (var session in sessions)
{
Fnsid.Add(session.SessionId);
Userid.Add(session.User.Id);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Userid.Add(session.User.Id);
UserId.Add(session.User.Id);


internal sealed class FriendScreenshots : XFireMessage
{
public FriendScreenshots(int userid, IEnumerable<Screenshot> screenshots) : base(XFireMessageType.FriendScreenshots)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public FriendScreenshots(int userid, IEnumerable<Screenshot> screenshots) : base(XFireMessageType.FriendScreenshots)
public FriendScreenshots(int userId, IEnumerable<Screenshot> screenshots) : base(XFireMessageType.FriendScreenshots)

{
foreach (var screen in screenshots)
{
UserId = userid;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
UserId = userid;
UserId = userId;

public FriendsOfFriendsRequest() : base(XFireMessageType.FriendsOfFriendsRequest) { }

[XMessageField("sid")]
public List<object> Sids { get; set; }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a Guid instead of an object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember correctly, the way that the serializer works it has to be a List but I will double check when I have time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List object*

var attribute = a.GetCustomAttribute<XMessageField>();
if (attribute != null)
{
if (attributeName.Length == 1)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand what problem this is solving?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the attribute name gets put into utf8 encoding.
Which is fine for strings, but for single byte attributes, the encoding part breaks certain hex values and causes it to return unexpected and unwanted results for Inbound packets with single bytes of certain values.
Chatroom packets suffered the worst with this, so I wrote this to return the exact byte given back so that it all smoothly gets decoded fine if the value is a single byte. It's ugly but it made it work.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, that makes a lot of sense. Instead of this special logic, I wonder if we can just use a 1 byte string encoding like ASCII instead of UTF8. We don't need any fancy characters for our field attribute names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as ASCII doesn't try to encode say 0x01 and treat it as a special byte like utf8 did, that's my only fear.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted this change to how it was and am able to update my status to trigger StatusChange (which has the byte type attribute) and it works fine.

Do you have a specific example where you ran into your issue?

Copy link
Contributor Author

@desukuran desukuran Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My memory isn't the best but I looked into some notes I took on this.

Most bytes should work, especially if they are near the letters portion of a utf8 charmap.

One of the examples I can think of is when the problem arised when inbound packets for User Group Member adding and User Request Advanced Info, which both contain 0x01 as an attribute name would deserialize and produce an attribute of \u0001 instead of the expected 0x01.

The problem arises when it hits specific utf8 special characters, this would fix the troublesome bytes that would break during incoming packet deserialization.

Copy link
Contributor

@Scatt Scatt May 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we put back the MessageSerializer.Deserialize to this

var attributeName = GetAttributeName(reader, messageType);
var field = fieldInfo
    .Where(a => a.GetCustomAttribute<XMessageField>() != null)
    .FirstOrDefault(a => a.GetCustomAttribute<XMessageField>()?.Name == attributeName);

Put back the MessageSerializer.GetAttributeName to return the encoded string

var count = messageTypeSet.Contains(messageType) ? 1 : reader.ReadByte();
var readBytes = reader.ReadBytes(count);
return Encoding.UTF8.GetString(readBytes);

Uncomment the call to the other constructor in XMessageField here to set the name property

public XMessageField(params byte[] name) 
    : this(Encoding.UTF8.GetString(name))
{
    NonTextualName = true;
    NameAsBytes = name;
}

Then the .FirstOrDefault above would act like this no?
a.GetCustomAttribute<XMessageField>()?.Name would be "\u0001"
attributeName would be "\u0001" as well

So it should match fine.

@@ -59,15 +77,36 @@ public static IMessage Deserialize(BinaryReader reader, IMessage messageBase)
return messageBase;
}

private static string GetAttributeName(BinaryReader reader, Type messageType)
private static byte[] GetAttributeName(BinaryReader reader, Type messageType)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand what problem these changes are solving?

[XMessageField("msg")]
public Dictionary<byte, object> MsgList { get; set; }

public override async Task Process(IXFireClient context)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the big refactor request, but we should do this like how we handle any other message from XFire.

Basically, each of these methods you have would be its own XFireMessage class where what you have as keys would be the XMessageField and its types. Then the process function can be the logic that you have in each method.

You'll want something similar to XFireMessageTypeFactory, but for your chatroom "CliMsg". We can create a IXFireMessageTypeFactory interface that the current XFireMessageTypeFactory implements and your new ChatroomMessageTypeFactory can implement.

Then we pass in the interface into MessageSerializer instead of the concrete type so that we can use that existing logic to serialize/deserialize your chatroom messages.

Let me know if you need some help with this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, I figured this part will be completely refactored but I gave it a shot anyway and made a working proof of concept.

I'll be in touch once I get to this in fixes.

var friendClient = context.Server.GetSession(user);
if (friendClient != null)
{
await friendClient.SendAndProcessMessage(new ChatroomUserJoined(ChatId, context.User, perms));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's confusing to me that this outbound message is creating more messages. What triggers this message? We should try to send these messages from there instead.

Ideally the outbound messages are doing one thing and processing THIS message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Join Info is the entire process of attempting to join a chatroom. From checking to see if it's locked down, to password entry to denying for various factors.

The message that gets sent, User joined is telling the other people in the chatroom that you joined if you passed all the checks and you join the chatroom.

If you want to, say keep the ethos of one packet per process, we could maybe return a successful join or something.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm let me think about this one more. Maybe I'll get a better understanding of what we can do with it as I read more of the code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do a write up soon to make it clear. Packet 25 is the all encompassing packet for chatroom actions from the client. So there's a LOT of moving parts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message is chatroom info you get sent when you join a chatroom?

Looks like this is only called from ChatroomInboundMessage so you could move the other logic in there, and it looks like it has a thing to send messages to everyone in chat ya?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants