Skip to content

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

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

Update constants/magic numbers #100

wants to merge 64 commits into from

Conversation

jonbarrow
Copy link
Member

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 as AutoMatchmakeOptionRecordLastGIDForParticipationCheck). 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:

AutoMatchmakeOption.None

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:

type MatchmakeOption uint32

type matchmakeOptions struct {
  None                               MatchmakeOption
  RecordLastGIDForParticipationCheck MatchmakeOption
  Reserved1                          MatchmakeOption
}

var MatchmakeOptions = matchmakeOptions{
	None:                               0,
	RecordLastGIDForParticipationCheck: 1,
	Reserved1:                          2,
}

This works, but we'd be accessing the data as MatchmakeOptions.None, where MatchmakeOptions is not technically the "real name" since that is being used up by the type MatchmakeOption

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

@DaniElectra
Copy link
Member

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

@jonbarrow
Copy link
Member Author

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:

  • Use the real name where at all possible
  • If the real name is likely to have conflicts and/or be confusing to use (such as with the Nothing value of GatheringFlags) then we prefix the values for the type with the types name

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

@DaniElectra
Copy link
Member

All of that sounds good to me 👍

@@ -0,0 +1,18 @@
package constants

// MatchmakeGeoIPResult represents an enum with an unknown use
Copy link
Member

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

Copy link
Member Author

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

@jonbarrow
Copy link
Member Author

jonbarrow commented Jul 11, 2025

@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 Build?

@DaniElectra
Copy link
Member

On the PR you say that:

// NotificationEvents represents the main category of a notification type.

But then on the Build function the return type is also NotificationEvents, and the subtype gets converted to NotificationEvents too for this. I think the return type should just be uint32 or something else, but the type conversions should be logical

@jonbarrow
Copy link
Member Author

@DaniElectra I've updated the return type to just be types.UInt32 so it can be used directly on the struct it's intended for

I've also pushed the first test of setting these constants to struct fields directly. I chose AutoMatchmakeParam for this just because it's the simplest, but I wanted to get your feedback on it. I had to make some changes to how the Copy and Equals functions work to support this change, which breaks some consistency, but the logic I think should be safe? We aren't using pointers anymore here and our new types are real types, not structs, so we should be able to just use normal operators for simple fields? Maybe we should consider moving all simple fields to doing this, as it would avoid extra function hops and stuff

@DaniElectra
Copy link
Member

I like that yeah! We could update every enum/flags parameter like this as it would integrate better in code

Maybe we should consider moving all simple fields to doing this, as it would avoid extra function hops and stuff

By this you mean updating every simple field (such as types.UInt32 to uint32)?

@jonbarrow
Copy link
Member Author

Maybe we should consider moving all simple fields to doing this, as it would avoid extra function hops and stuff

By this you mean updating every simple field (such as types.UInt32 to uint32)?

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 Equals method was mostly useful before the types update, and now is only really useful when either dealing with a complex struct (like SourceMatchmakeSession here) or with an instance of a RVType where we don't know the underlying type at runtime

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

@jonbarrow
Copy link
Member Author

@DaniElectra In MatchmakeSessionSearchCriteria, the m_MatchmakeSystemType field is handled as a string for some reason but the value is a normal integer enum. Should we change the type of MatchmakeSessionSearchCriteria.MatchmakeSystemType from types.String to constants.MatchmakeSystemType? This would make it slightly inaccurate to the NEX type, but would make for easier integrations

@DaniElectra
Copy link
Member

DaniElectra commented Jul 13, 2025

Maybe we should consider moving all simple fields to doing this, as it would avoid extra function hops and stuff

By this you mean updating every simple field (such as types.UInt32 to uint32)?

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 Equals method was mostly useful before the types update, and now is only really useful when either dealing with a complex struct (like SourceMatchmakeSession here) or with an instance of a RVType where we don't know the underlying type at runtime

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

Ohh okay, that sounds good to me!

In MatchmakeSessionSearchCriteria, the m_MatchmakeSystemType field is handled as a string for some reason but the value is a normal integer enum

That's because it is a string on MatchmakeSessionSearchCriteria, so that it's an optional parameter when searching

@jonbarrow
Copy link
Member Author

In MatchmakeSessionSearchCriteria, the m_MatchmakeSystemType field is handled as a string for some reason but the value is a normal integer enum

That's because it is a string on MatchmakeSessionSearchCriteria, so that it's an optional parameter when searching

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. MatchmakeSystemTypeInvalid exists but it's a "valid" type despite the name, it's not the "doesn't exist" type we'd need

Maybe we can just add a custom value to the enum that indicates if the value is missing or not?

@DaniElectra
Copy link
Member

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 m_MatchmakeSystemType field (not the m_GameMode) on the MatchmakeSession to a custom "invalid" value to avoid matching with vanilla users. This worked on Nintendo servers, so I believe that behavior should remain the same on our servers too

@jonbarrow
Copy link
Member Author

Also important note before we forget, we don't want to add validation checks for every field. For example, CTGP-7 modifies the m_MatchmakeSystemType field (not the m_GameMode) on the MatchmakeSession to a custom "invalid" value to avoid matching with vanilla users. This worked on Nintendo servers, so I believe that behavior should remain the same on our servers too

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 IsValid methods to subtypes, opting to just let the RMC handler do it, so maybe that should be done everywhere?

@DaniElectra
Copy link
Member

Also important note before we forget, we don't want to add validation checks for every field. For example, CTGP-7 modifies the m_MatchmakeSystemType field (not the m_GameMode) on the MatchmakeSession to a custom "invalid" value to avoid matching with vanilla users. This worked on Nintendo servers, so I believe that behavior should remain the same on our servers too

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 IsValid methods to subtypes, opting to just let the RMC handler do it, so maybe that should be done everywhere?

That sounds good to me 👍

@jonbarrow jonbarrow force-pushed the chore/magic-numbers branch from 4a11d80 to d8468a8 Compare August 6, 2025 21:59
@jonbarrow
Copy link
Member Author

(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 {
Copy link
Member

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

Copy link
Member Author

@jonbarrow jonbarrow Aug 7, 2025

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?

Copy link
Member

@DaniElectra DaniElectra Aug 23, 2025

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

Copy link
Member Author

@jonbarrow jonbarrow Aug 23, 2025

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 in uiType types.UInt32, uiParam1 types.UInt64, uiParam2 types.UInt64, strParam types.String
  • GetFriendNotificationData - Takes in uiType 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

Copy link
Member

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

Copy link
Member Author

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:

  1. Accept that the official name for the categories type will be misused
  2. 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)

Copy link
Member

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 👍

@jonbarrow jonbarrow marked this pull request as ready for review August 23, 2025 19:50
@jonbarrow
Copy link
Member Author

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 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, and Ranking2CategorySetting.ResetDay which denotes the day of the week/month to reset seasons on

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

@jonbarrow
Copy link
Member Author

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?

Copy link
Member

@DaniElectra DaniElectra left a 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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Package types implements all the types used by the Shop protocol
// Package types implements all the types used by the Subscriber protocol

Comment on lines +3 to +6
// AnybodyParticipationPolicyArgument seems to determine whether or not to
// close participation when the gathering owner changes when MatchmakeSystemTypeAnybody
// is used?
type AnybodyParticipationPolicyArgument = PolicyArgument
Copy link
Member

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

Copy link
Member Author

@jonbarrow jonbarrow Aug 24, 2025

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

@jonbarrow
Copy link
Member Author

You mention day of the week/month, does this mean that these two cases are represented under the same field.

Correct. Depending on the reset mode used, the resetDay value can either correspond to a day of the week (in the weekly reset mode) or a calendar day in an enabled month (in the monthly reset mode)

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

@DaniElectra
Copy link
Member

Also since I saw you fixed GetObjectInfos, reminder that ResetRatings also needs to be fixed

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

Yeah that seems fair enough

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.

2 participants