-
Notifications
You must be signed in to change notification settings - Fork 13
Update constants/magic numbers #100
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
base: master
Are you sure you want to change the base?
Conversation
IMO I prefer using Go constants over maps for these magic numbers, since it allows for reading the underlying value directly on IDE and is in line with making idiomatic code, which is part of our goal PretendoNetwork/nex-go#73 . It may not be the cleanest, but I think it integrates better |
This is fair 👍 in that case I propose the following standard:
Also do we want to go ahead and update the notifications types to be consistent with the rest of the protocols constants, especially now that we have the real names? It would be a breaking change but I've already started renaming certain things here anyway so this whole PR is breaking already |
All of that sounds good to me 👍 |
@@ -0,0 +1,18 @@ | |||
package constants | |||
|
|||
// MatchmakeGeoIPResult represents an enum with an unknown use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is probably part of the @GIR
parameter of MatchmakeParam, representing the result of IP Geolocation when it has been requested by the game. I have documented some of the params here (also relevant for the ScoreBased
selection method): https://gist.github.com/DaniElectra/b0f70018a71cadb329e5fa972a837ee2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems likely. Good info!
@SR
| Unknown | Bool | Unknown purpose, only seen in responses
Maybe this stands for ServerResponse or something? Unsure what the use is, but that name would make sense
Speaking of ScoreBased
, I do have some theories about how it works but nothing concrete so I could be entirely wrong. I couldn't conclude anything definitive, but in my testing prior to the shutdown I was able to seemingly manipulate the matchamaking responses using MatchmakeParam
when using the ScoreBased
mode. I believe that there's some game-specific logic done on the server that assigns a "score"/"weight" to certain parameters, and the server does some sort of calculation on them, and then picks the session with the best "score"?
That would lineup pretty well with some of the information you have here too. I could could see this comparing stuff like disconnection/violation rates, countries/geo locations, etc. to find a session that best works for each specific client? Some of this data seems to be reported by the MatchmakeReferee
protocol as well
Assuming that's true, maybe @TS
stands for "TotalScore", as calculated by the server, which is why it's only seen in responses? Also assuming this is true, I think @SI
might not refer to an attribute index like the other indexes we've see, since these would be internal values. Maybe games could have multiple "scores"/"weights" for certain values and this "index" picks which set of values to use, similar to how DataStore
can have multiple configs? I could see this being useful for having different configurations for different "types" of sessions. Like maybe a game implements "normal" sessions with a higher weight on geo IP so users are more likely to match with people around them, but lowers the geo IP weight for, say, global tournaments so you have a higher chance of seeing people not around you?
Of course (assuming the above is accurate) we have no way of knowing what the calculation algorithm is, or what weights/scores are assigned for each game, or anything like that. So not a super useful mode for us I think
@DaniElectra I did some playing around with some ideas for notifications, how does this look? package main
import "fmt"
type NotificationEvents uint32
type subType uint32 // Exists strictly to limit the types of values that can be passed to NotificationEvents.Build
func (ne NotificationEvents) Build(subtype ...subType) NotificationEvents {
category := ne * 1000
if len(subtype) == 0 {
return category
}
return category + NotificationEvents(subtype[0])
}
const (
NotificationEventsParticipationEvent NotificationEvents = 3
)
type ParticipationEvents = subType
const (
ParticipationEventsParticipate ParticipationEvents = 1
)
func main() {
notificationTypeA := NotificationEventsParticipationEvent
notificationTypeB := NotificationEventsParticipationEvent.Build()
notificationTypeC := NotificationEventsParticipationEvent.Build(ParticipationEventsParticipate)
fmt.Println(notificationTypeA) // 3
fmt.Println(notificationTypeB) // 3000
fmt.Println(notificationTypeC) // 3001
} I think this helps clean up some of the notification handling, though I'm a bit iffy on the name |
On the PR you say that:
But then on the |
@DaniElectra I've updated the return type to just be I've also pushed the first test of setting these constants to struct fields directly. I chose |
I like that yeah! We could update every enum/flags parameter like this as it would integrate better in code
By this you mean updating every simple field (such as |
No I meant like going from this: func (amp AutoMatchmakeParam) Equals(o types.RVType) bool {
if _, ok := o.(AutoMatchmakeParam); !ok {
return false
}
other := o.(AutoMatchmakeParam)
if amp.StructureVersion != other.StructureVersion {
return false
}
if !amp.SourceMatchmakeSession.Equals(other.SourceMatchmakeSession) {
return false
}
if !amp.AdditionalParticipants.Equals(other.AdditionalParticipants) {
return false
}
if !amp.GIDForParticipationCheck.Equals(other.GIDForParticipationCheck) {
return false
}
if amp.AutoMatchmakeOption != other.AutoMatchmakeOption {
return false
}
if !amp.JoinMessage.Equals(other.JoinMessage) {
return false
}
if !amp.ParticipationCount.Equals(other.ParticipationCount) {
return false
}
if !amp.LstSearchCriteria.Equals(other.LstSearchCriteria) {
return false
}
return amp.TargetGIDs.Equals(other.TargetGIDs)
} to this: func (amp AutoMatchmakeParam) Equals(o types.RVType) bool {
if _, ok := o.(AutoMatchmakeParam); !ok {
return false
}
other := o.(AutoMatchmakeParam)
if amp.StructureVersion != other.StructureVersion {
return false
}
if !amp.SourceMatchmakeSession.Equals(other.SourceMatchmakeSession) {
return false
}
if !slices.Equal(amp.AdditionalParticipants, other.AdditionalParticipants) {
return false
}
if amp.GIDForParticipationCheck != other.GIDForParticipationCheck {
return false
}
if amp.AutoMatchmakeOption != other.AutoMatchmakeOption {
return false
}
if amp.JoinMessage != other.JoinMessage {
return false
}
if !amp.ParticipationCount != other.ParticipationCount {
return false
}
if !slices.Equal(amp.LstSearchCriteria, other.LstSearchCriteria) {
return false
}
return slices.Equal(amp.TargetGIDs, other.TargetGIDs)
} The use of the Since our simple types (UInt32, String, etc.) are all type definitions based on real, basic, types and not structs, we can make use of standard operators/methods on them in places like this |
@DaniElectra In |
Ohh okay, that sounds good to me!
That's because it is a string on |
I see. How do you want to handle that then? That makes it rather annoying since it would be nice to be able to reuse the existing constant here, but we'd need to handle cases where it doesn't exist. Maybe we can just add a custom value to the enum that indicates if the value is missing or not? |
I think it can remain as it is, allowing us to handle it as we consider on the common protocols. We have to parse the value somewhere, and I'd prefer to do it in the common protocols than adding a fictional enum value just to parse it here Also important note before we forget, we don't want to add validation checks for every field. For example, CTGP-7 modifies the |
I can opt to just remove the validation checks entirely if you think that would be better. We can handle all of that on the receiving end if need be? I've already removed validation in one place in match making (not yet pushed) because I started using a real type alias which made it impossible to add multiple |
That sounds good to me 👍 |
…th the official naming
4a11d80
to
d8468a8
Compare
(rebased the branch with the latest changes) |
// Build creates the final notification type ID used in NotificationEvent.m_uiType. | ||
// | ||
// Takes an optional subtype. Only the first subtype defined is used. | ||
func (ne NotificationEvents) Build(subtype ...subType) types.UInt32 { | ||
category := types.UInt32(ne * 1000) | ||
func (ne NotificationEvents) Build(subtype ...subType) NotificationEvents { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should split the type into the categories and the full type with the subtype. Mixing these types together is counter-intuitive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you mean? This was done because NotificationEvent.Type
is now of type constants.NotificationEvents
. So when we build the NotificationEvent
instance we would do this directly:
notification := notifications_types.NewNotificationEvent()
notification.Type = notifications_constants.NotificationEventsSessionLaunched.Build()
If it was kept as a types.UInt32
then we would have to explicitly cast the return value every time:
notification := notifications_types.NewNotificationEvent()
notification.Type = notifications_constants.NotificationEvents(notifications_constants.NotificationEventsSessionLaunched.Build())
I'm not sure what you mean by counter-intuitive, or where this splitting should be done? The types are already split by category and subtype, and the usage of the method seems intuitive to me?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestion was to split the NotificationEvents
type, NotificationCategory
for the actual category and NotificationEvents
for the category + subtype. The Build
function would look like this:
func (nc NotificationCategory) Build(subtype ...SubType) NotificationEvents
This way we wouldn't have to cast into the notification.Type
and the separation between "category" and "category + subtype" is more intuitive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only issue with that is that NotificationEvents
is the official name of these category types https://github.com/tech-ticks/RTDXTools/blob/232a7797e01369e9c12704f58fdde65dd3ac1c32/Assets/Scripts/Stubs/Generated/Assembly-CSharp/NexPlugin/Common.cs#L35-L53, so we would have to intentionally not use the correct official name and instead repurpose the official name for our own definition
We have changed official names before, but that was due to technical issues (see RatingLockPeriodDay
, which is officially named RatingLockPeriod
but that conflicts with RatingLockPeriod
in RatingLockType
)
Though I do agree that that sounds more intuitive, especially since I had added these types to the MatchmakeExtension
protocol (see 2e35b3a). I ran into a snag there because there's 3 methods that take in notification data:
UpdateNotificationData
- Takes inuiType types.UInt32, uiParam1 types.UInt64, uiParam2 types.UInt64, strParam types.String
GetFriendNotificationData
- Takes inuiType types.Int32
DebugNotifyEvent
-pid types.PID, mainType types.UInt32, subType types.UInt32, param1 types.UInt64, param2 types.UInt64, stringParam types.String
I only ended up changing DebugNotifyEvent
to take in mainType notifications_constants.NotificationEvents, subType notifications_constants.SubType
because the other 2 made this a bit annoying. UpdateNotificationData
seems to expect the fully finished built type (which isn't something we have types for at all) and GetFriendNotificationData
wants a signed version of this?
Renaming NotificationEvents
to NotificationCategory
would make handling UpdateNotificationData
a bit better since like you said it splits the final built type from the category, but it still has the issue of "we don't have types for these built values", and it doesn't help GetFriendNotificationData
at all
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UpdateNotificationData
seems to expect the fully finished built type (which isn't something we have types for at all)
That isn't the case, UpdateNotificationData
only uses the category as input
GetFriendNotificationData
being signed is an issue though, huh. I guess we can just leave things as is, no big deal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UpdateNotificationData
seems to expect the fully finished built type (which isn't something we have types for at all)That isn't the case,
UpdateNotificationData
only uses the category as input
GetFriendNotificationData
being signed is an issue though, huh. I guess we can just leave things as is, no big deal
That actually makes things easier then tbh. We can definitely still do this if you want, we just have to:
- Accept that the official name for the categories type will be misused
- Create a signed version of the categories
We can make the signed part a bit easier tbh by just creating a 2nd type for it. Like:
package constants
type NotificationCategory uint32
type NotificationCategorySigned int32
func (nc NotificationCategory) ToSigned() NotificationCategorySigned {
return NotificationCategorySigned(nc)
}
func (ncs NotificationCategorySigned) ToUnsigned() NotificationCategory {
return NotificationCategory(ncs)
}
const (
NotificationCategorySessionLaunched NotificationCategory = 2
NotificationCategoryParticipationEvent NotificationCategory = 3
NotificationCategoryOwnershipChangeEvent NotificationCategory = 4
NotificationCategoryGameNotification1 NotificationCategory = 101
NotificationCategoryGameNotification2 NotificationCategory = 102
NotificationCategoryGameNotification3 NotificationCategory = 103
NotificationCategoryGameNotification4 NotificationCategory = 104
NotificationCategoryGameNotification5 NotificationCategory = 105
NotificationCategoryGameNotification6 NotificationCategory = 106
NotificationCategoryGameNotification7 NotificationCategory = 107
NotificationCategoryGameNotification8 NotificationCategory = 108
NotificationCategoryGatheringUnregistered NotificationCategory = 109
NotificationCategoryHostChangeEvent NotificationCategory = 110
NotificationCategoryGameNotificationLogout NotificationCategory = 111
NotificationCategorySubscriptionEvent NotificationCategory = 112
NotificationCategoryGameServerMaintenance NotificationCategory = 113
NotificationCategoryMaintenanceAnnouncement NotificationCategory = 114
NotificationCategoryServiceItemRequestCompleted NotificationCategory = 115
NotificationCategoryRoundStarted NotificationCategory = 116
NotificationCategoryFirstRoundReportReceived NotificationCategory = 117
NotificationCategoryRoundSummarized NotificationCategory = 118
NotificationCategoryMatchmakeSystemConfigurationNotification NotificationCategory = 119
NotificationCategoryMatchmakeSessionSystemPasswordSet NotificationCategory = 120
NotificationCategoryMatchmakeSessionSystemPasswordClear NotificationCategory = 121
NotificationCategoryAddedToGathering NotificationCategory = 122
NotificationCategoryUserStatusUpdatedEvent NotificationCategory = 128
NotificationCategoryEagleAddress NotificationCategory = 200
)
GetFriendNotificationData
would just take in NotificationCategorySigned
and we could call ToUnsigned
in the implementation handler (or just directly cast)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds good to me 👍
I believe this is ready for review now. I don't THINK I missed anything? All the protocols which have known constants should have them now, I think, as well as using those constants in the protocols themselves If I missed any constants do let me know Also I wanted to ask: there's a couple values in It might be useful to create bespoke types for these, despite them not having true types? Let me know what you think @DaniElectra after you go through them, I'll be happy to add any bespoke types to whatever protocol if you think it's helpful |
Actually I lied, I missed the Nintendo variant of the notifications protocol. How do we want to handle that? We have no known names for any of those constants, just make something up I guess? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have only skimmed through the changes, but I think everything should be good
Also I wanted to ask: there's a couple values in Ranking2 which don't have constants, but might benefit from having them? Such as Ranking2CategorySetting.ResetMonth, which seems to be a 12-bit flag set for enabling months to reset seasons on
I agree with that, this example could benefit from having a dedicated type
and Ranking2CategorySetting.ResetDay which denotes the day of the week/month to reset seasons on
You mention day of the week/month, does this mean that these two cases are represented under the same field. If that is the case, then we can add a dedicated constant here too
Actually I lied, I missed the Nintendo variant of the notifications protocol. How do we want to handle that? We have no known names for any of those constants, just make something up I guess?
I'd say to make something up yeah
@@ -0,0 +1,111 @@ | |||
// Package types implements all the types used by the Shop protocol |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Package types implements all the types used by the Shop protocol | |
// Package types implements all the types used by the Subscriber protocol |
// AnybodyParticipationPolicyArgument seems to determine whether or not to | ||
// close participation when the gathering owner changes when MatchmakeSystemTypeAnybody | ||
// is used? | ||
type AnybodyParticipationPolicyArgument = PolicyArgument |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this possibly be related to the ParticipationPolicyOpenParticipation
instead of MatchmakeSystemTypeAnybody
? Maybe this could mean that the participation policy is actually named ParticipationPolicyAnybody
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the matchmaking client there's 2 ways the game can set these policies:
MatchmakeSession::SetMatchmakeSystemType(MatchmakeSystemType system_type, PolicyArgument policy_arg)
Gathering::SetParticipationPolicy(ParticipationPolicy policy, PolicyArgument policy_arg)
When MatchmakeSession::SetMatchmakeSystemType
is called and system_type
is either MatchmakeSystemTypeAnybody
or MatchmakeSystemTypeFriends
, then the client internally calls Gathering::SetParticipationPolicy(ParticipationPolicyFriendsOnly, policy_arg)
Otherwise the client internally calls Gathering::SetParticipationPolicy(ParticipationPolicyOpenParticipation, 0)
At least that's how it works in Xenoblade. ParticipationPolicyOpenParticipation
never seems to get a policy argument for some reason:
void SetMatchmakeSystemType__Q3_2nn3nex16MatchmakeSessionFQ3_2nn3nex19MatchmakeSystemTypeUi(nn::nex::MatchmakeSession *this, uint system_type, uint policy_arg) {
this->m_MatchmakeSystemType = system_type;
if ((1 < system_type) && (system_type < 4)) {
SetParticipationPolicy__Q3_2nn3nex9GatheringFUiT1((nn::nex::Gathering *)this, 0x62, policy_arg);
return;
}
SetParticipationPolicy__Q3_2nn3nex9GatheringFUiT1((nn::nex::Gathering *)this, 8, 0);
return;
}
void SetParticipationPolicy__Q3_2nn3nex9GatheringFUiT1(nn::nex::Gathering *this, undefined4 participation_policy, undefined4 policy_argument) {
this->participation_policy = participation_policy;
this->policy_argument = policy_argument;
return;
}
I have no idea why it sets ParticipationPolicyFriendsOnly
for both MatchmakeSystemTypeAnybody
AND MatchmakeSystemTypeFriends
though, but given the naming of Anybody
in both AnybodyParticipationPolicyArgument
and MatchmakeSystemTypeAnybody
, and from what I remember in my testing, this policy argument seems directly related to MatchmakeSystemTypeAnybody
Correct. Depending on the reset mode used, the There will certainly be some overlap with values (for example I believe the weekday mode uses values 0-6, whereas the calendar days use 1-28, so the "2nd of the month" constant and "Wednesday" constant will both be value 2) but I don't think this would be an issue given that you also have the context of what mode is being used |
Also since I saw you fixed
Yeah that seems fair enough |
Resolves #XXX
Changes:
Draft for now as I want to go through and add all the missing values, as well as update any existing ones that need it.
There's a lot of work going on in the common protocols lib that is using magic numbers, such as:
This PR aims to add all the missing magic numbers/constants/enums so that this doesn't keep happening. Nearly all the data comes from https://github.com/tech-ticks/RTDXTools/, with some RE/guess work mixed in.
I would also like to discuss the possibility of moving towards maps for these enums/flags. Go doesn't have real enums, and that does make it a little awkward to use/reference them since it means we either risk name collisions for simple names (such as
Nothing
) or we end up with very long-winded names (such asAutoMatchmakeOptionRecordLastGIDForParticipationCheck
). So far I have opted to be inaccurate in places and ALWAYS prefix a value with its type name, even if that wasn't the case officially, so I'm not concerned about accuracy here.By moving to maps we can potentially use code like this:
Which is a bit cleaner imo.
We already treat some types this way, such as:
The only question then, if we do decide to do this, is what to do about the map/struct/type names. For example, do we forego the "real" names in places for code like this:
This works, but we'd be accessing the data as
MatchmakeOptions.None
, whereMatchmakeOptions
is not technically the "real name" since that is being used up by the typeMatchmakeOption
Also, we should discuss moving some of the struct field types to these known types now that we have them since it would make certain code a bit cleaner due to not needing to cast when comparing to constants