-
Notifications
You must be signed in to change notification settings - Fork 16
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
[Enhancement]: Everything we know about AnyDataHolder
is wrong
#74
Comments
Haven't read and understood everything yet but just to check youre using ghs-demangle for those C++ types right? Tools meant for gcc (ghidra, c++filt) can give incorrect decoding results when used on ghs symbols |
Yes, I use https://github.com/Chadderz121/ghs-demangle for everything. My Ghidra setup doesn't even automatically demangle Wii U symbols at all |
I did some testing with the latest proposal and it seems that you would not be able to call type DataHolder[T DataInterface] AnyObjectHolder[T, String] This means that to call holder1 := NewCustomDataHolder(UserMessage{})
holder11 := AnyObjectHolder[UserMessage, String](holder1.DataHolder)
holder11.WriteTo() Using a type alias for a generic type is possible, and would make the code look better: type DataHolder[T DataInterface] = AnyObjectHolder[T, String]
holder1 := NewCustomDataHolder(UserMessage{})
holder1.WriteTo() However, this feature is locked in Go behind an experimental flag Also, I'm not sure how would this work for reading an object holder? If we do |
Due to the above mentioned issues, and in the spirit of #73 to reduce "1:1" ports in favor of "follow the intentions" implementations, I've opted to change this proposal to be simpler than the way Nintendo/Quazal implemented this. Our basic usage requirements are:
With this in mind, here is an updated demo. This demo:
I think this demonstrates everything we'd need this to do, while not being 1:1 with the decomp? Which I think is a good first step to #73? package main
import "fmt"
type RVType interface{}
type HoldableObject interface {
RVType
ObjectID() any
}
var AnyObjectHolderObjects = make(map[string]HoldableObject)
type AnyObjectHolder[T HoldableObject] struct {
Object T
}
func (aoh AnyObjectHolder[T]) WriteTo() {
fmt.Println(aoh.Object.ObjectID())
fmt.Printf("%+v\n", aoh.Object)
}
func (aoh *AnyObjectHolder[T]) ExtractFrom() {
objectName := "UserMessage" // * Simulate reading the string here
object := AnyObjectHolderObjects[objectName] // * Simulate the "CopyRef()"/"Deref()" calls
aoh.Object = object.(T)
}
type Data struct{}
func (d Data) ObjectID() any {
return "Data"
}
type DataInterface interface {
HoldableObject
DataObjectID() any
}
type DataHolder = AnyObjectHolder[DataInterface]
type UserMessage struct {
Data
m_uiID uint32
}
func (um UserMessage) ObjectID() any {
return um.DataObjectID()
}
func (um UserMessage) DataObjectID() any {
return "UserMessage"
}
type Gathering struct {
m_idMyself uint32
}
func (g Gathering) ObjectID() any {
return g.GatheringObjectID()
}
func (g Gathering) GatheringObjectID() any {
return "Gathering"
}
type GatheringInterface interface {
HoldableObject
GatheringObjectID() any
}
type GatheringHolder = AnyObjectHolder[GatheringInterface]
type MatchmakeSession struct {
Gathering
m_GameMode uint32
}
func (ms MatchmakeSession) ObjectID() any {
return ms.GatheringObjectID()
}
func (ms MatchmakeSession) GatheringObjectID() any {
return "MatchmakeSession"
}
func main() {
AnyObjectHolderObjects["UserMessage"] = UserMessage{}
AnyObjectHolderObjects["MatchmakeSession"] = MatchmakeSession{}
ReadDemo()
WriteDemo()
}
func ReadDemo() {
// * Like in the request for TicketGranting::DeliverMessageMultiTarget
oUserMessage := DataHolder{}
oUserMessage.ExtractFrom()
fmt.Printf("%+v\n", oUserMessage) // * {Object:{Data:{} m_uiID:0}}
}
func WriteDemo() {
// * Like in the response for MatchmakeExtension::AutoMatchmake_Postpone
joinedGathering := GatheringHolder{
Object: MatchmakeSession{}, // * Dummy data
}
joinedGathering.WriteTo() // * Logs:
// * MatchmakeSession
// * {Gathering:{m_idMyself:0} m_GameMode:0}
joinedGathering = GatheringHolder{
Object: Gathering{}, // * Dummy data
}
joinedGathering.WriteTo() // * Logs:
// * Gathering
// * {m_idMyself:0}
} |
This looks good, though I think we can still handle multiple identifiers for the sake of compatibility (in case there's some random QRV game that doesn't use strings) by using a single package main
import "fmt"
type RVType interface{}
type String string
type HoldableObject interface {
RVType
ObjectID() RVType
}
var AnyObjectHolderObjects = make(map[RVType]HoldableObject)
type AnyObjectHolder[T HoldableObject] struct {
Object T
}
func (aoh AnyObjectHolder[T]) WriteTo() {
fmt.Println(aoh.Object.ObjectID())
fmt.Printf("%+v\n", aoh.Object)
}
func (aoh *AnyObjectHolder[T]) ExtractFrom() {
objectName := String("UserMessage") // * Simulate reading the string here
object := AnyObjectHolderObjects[objectName] // * Simulate the "CopyRef()"/"Deref()" calls
aoh.Object = object.(T)
}
type Data struct{}
func (d Data) ObjectID() RVType {
return d.DataObjectID()
}
func (d Data) DataObjectID() RVType {
return String("Data")
}
type DataInterface interface {
HoldableObject
DataObjectID() RVType
}
type DataHolder = AnyObjectHolder[DataInterface]
type UserMessage struct {
Data
m_uiID uint32
}
func (um UserMessage) ObjectID() RVType {
return um.DataObjectID()
}
func (um UserMessage) DataObjectID() RVType {
return String("UserMessage")
}
type Gathering struct {
m_idMyself uint32
}
func (g Gathering) ObjectID() RVType {
return g.GatheringObjectID()
}
func (g Gathering) GatheringObjectID() RVType {
return String("Gathering")
}
type GatheringInterface interface {
HoldableObject
GatheringObjectID() RVType
}
type GatheringHolder = AnyObjectHolder[GatheringInterface]
type MatchmakeSession struct {
Gathering
m_GameMode uint32
}
func (ms MatchmakeSession) ObjectID() RVType {
return ms.GatheringObjectID()
}
func (ms MatchmakeSession) GatheringObjectID() RVType {
return String("MatchmakeSession")
}
func main() {
AnyObjectHolderObjects[String("UserMessage")] = UserMessage{}
AnyObjectHolderObjects[String("MatchmakeSession")] = MatchmakeSession{}
ReadDemo()
WriteDemo()
}
func ReadDemo() {
// * Like in the request for TicketGranting::DeliverMessageMultiTarget
oUserMessage := DataHolder{}
oUserMessage.ExtractFrom()
fmt.Printf("%+v\n", oUserMessage) // * {Object:{Data:{} m_uiID:0}}
}
func WriteDemo() {
// * Like in the response for MatchmakeExtension::AutoMatchmake_Postpone
joinedGathering := GatheringHolder{
Object: MatchmakeSession{}, // * Dummy data
}
joinedGathering.WriteTo() // * Logs:
// * MatchmakeSession
// * {Gathering:{m_idMyself:0} m_GameMode:0}
joinedGathering = GatheringHolder{
Object: Gathering{}, // * Dummy data
}
joinedGathering.WriteTo() // * Logs:
// * Gathering
// * {m_idMyself:0}
} |
My main concern with using multiple identifiers is how we actually tell the server which one to use. This isn't so much of an issue when writing objects, but for reading them it is. The reader needs to know beforehand what type of identifier to use, otherwise it won't be able to make the copies it needs to. This can't be done on a per-object basis either because you need to know what object is being used before doing anything The only thing I can think of is, like, a global config? Like we do other stream settings like the PID size? But then how would that API look? The |
Oh right I missed that. In that case we can also include the identifier on the generics like originally intended without too much hassle, like shown in the following code. If we think this is getting too complex, I'm fine with going back to your original design package main
import "fmt"
type RVType interface {
CopyRef() RVTypePtr
}
type RVTypePtr interface {
RVType
ExtractFrom()
Deref() RVType
}
type String string
func (s *String) ExtractFrom() {
*s = String("UserMessage")
}
func (s String) CopyRef() RVTypePtr {
var copied String = s
return &copied
}
func (s *String) Deref() RVType {
return *s
}
type HoldableObject interface {
RVType
ObjectID() RVType
}
var AnyObjectHolderObjects = make(map[RVType]HoldableObject)
type AnyObjectHolder[T HoldableObject, I RVType] struct {
Object T
ObjectID I
}
func (aoh AnyObjectHolder[T, I]) WriteTo() {
fmt.Println(aoh.ObjectID)
fmt.Printf("%+v\n", aoh.Object)
}
func (aoh *AnyObjectHolder[T, I]) ExtractFrom() {
objectID := aoh.ObjectID.CopyRef() // * Simulate reading the string here
objectID.ExtractFrom()
aoh.ObjectID = objectID.Deref().(I)
object := AnyObjectHolderObjects[aoh.ObjectID] // * Simulate the "CopyRef()"/"Deref()" calls
aoh.Object = object.(T)
}
type Data struct{}
func (d Data) ObjectID() RVType {
return d.DataObjectID()
}
func (d Data) DataObjectID() RVType {
return String("Data")
}
type DataInterface interface {
HoldableObject
DataObjectID() RVType
}
type DataHolder = AnyObjectHolder[DataInterface, String]
type UserMessage struct {
Data
m_uiID uint32
}
func (um UserMessage) ObjectID() RVType {
return um.DataObjectID()
}
func (um UserMessage) DataObjectID() RVType {
return String("UserMessage")
}
func (um *UserMessage) ExtractFrom() {}
func (um *UserMessage) Deref() RVType {
return *um
}
func (um UserMessage) CopyRef() RVTypePtr {
copied := um
return &copied
}
type Gathering struct {
m_idMyself uint32
}
func (g Gathering) ObjectID() RVType {
return g.GatheringObjectID()
}
func (g Gathering) GatheringObjectID() RVType {
return String("Gathering")
}
func (g *Gathering) ExtractFrom() {}
func (g *Gathering) Deref() RVType {
return *g
}
func (g Gathering) CopyRef() RVTypePtr {
copied := g
return &copied
}
type GatheringInterface interface {
HoldableObject
GatheringObjectID() RVType
}
type GatheringHolder = AnyObjectHolder[GatheringInterface, String]
type MatchmakeSession struct {
Gathering
m_GameMode uint32
}
func (ms MatchmakeSession) ObjectID() RVType {
return ms.GatheringObjectID()
}
func (ms MatchmakeSession) GatheringObjectID() RVType {
return String("MatchmakeSession")
}
func (ms *MatchmakeSession) ExtractFrom() {}
func (ms *MatchmakeSession) Deref() RVType {
return *ms
}
func (ms MatchmakeSession) CopyRef() RVTypePtr {
copied := ms
return &copied
}
func main() {
AnyObjectHolderObjects[String("UserMessage")] = UserMessage{}
AnyObjectHolderObjects[String("MatchmakeSession")] = MatchmakeSession{}
ReadDemo()
WriteDemo()
}
func ReadDemo() {
// * Like in the request for TicketGranting::DeliverMessageMultiTarget
oUserMessage := DataHolder{}
oUserMessage.ExtractFrom()
fmt.Printf("%+v\n", oUserMessage) // * {Object:{Data:{} m_uiID:0}}
}
func WriteDemo() {
matchmakeSession := MatchmakeSession{}
// * Like in the response for MatchmakeExtension::AutoMatchmake_Postpone
joinedGathering := GatheringHolder{
Object: matchmakeSession, // * Dummy data
ObjectID: matchmakeSession.ObjectID().(String),
}
joinedGathering.WriteTo() // * Logs:
// * MatchmakeSession
// * {Gathering:{m_idMyself:0} m_GameMode:0}
gathering := Gathering{}
joinedGathering = GatheringHolder{
Object: gathering, // * Dummy data
ObjectID: gathering.ObjectID().(String),
}
joinedGathering.WriteTo() // * Logs:
// * Gathering
// * {m_idMyself:0}
} |
I guess part of the issue here stems from the fact that we haven't actually seen anything besides strings being used here. Which means we have to make assumptions on how this works There's 2 possibilities here:
That changes the implementation here If the way this works is option 2, then what you've done here should work perfectly. It would allow the built in objects to remain how they are, but custom ones to do whatever is they do. At least, that's the case for using the raw However if it's option 1, then what you've done here wouldn't work since it still always assumes a string for the built in types Option 1 is how I assumed things worked, which is why I was mentioning a "global config" so that we could change how built in types work. But it seems like you assumed it worked like option 2, where we just always use strings but custom types may use something else It might be worth making a "needs additional research" issue here for how the identifiers work, and then holding off on implementing "custom identifiers" until later, and always assume strings for now. We can still keep some of the foundations of this though, like letting the map of registered types take in |
That sounds good to me, we can go with your design for now and use |
Checked Existing
What enhancement would you like to see?
Currently we implement
AnyDataHolder
as documented by Kinnay originally https://nintendo-wiki.pretendo.network/docs/nex/types#anydataholderThis is wrong, however, and doesn't make sense in places like the matchmaking protocols where
AnyDataHolder<Gathering>
and the like is frequently seen (sinceGathering
does not inherit fromData
). We should probably change this to be more accurate, as well as update the docs to reflect thisThere are actually several classes/templates used here for this and
AnyDataHolder
is NOT a real type. The notes here will be broken up into relevant sectionsAll data is taken from either Xenoblade or Minecraft on the Wii U
This was initially discovered in July 2024 and discussion/research/drafts continued into August of 2024 however we never published these findings outside of our private Discord channel. This issue is designed to bring these notes input the public eye so further discussion can happen with them
Classes/concepts
AnyObjectHolder
- Template class used to hold any object. Takes in 2 types, the base class for the type to be held and a 2nd "identifier" type. In practice the "identifier" type is alwaysnn::nex::String
, and is likely used to write the data we currently document asType name
(it's possible that other types are supported here but was never officially used)AnyObjectAdapter
- Seems to be just a barebones adapter class?AnyDataAdapter
- Seems to be an adapter class to conform toData
?AnyGatheringAdapter
- Seems to be an adapter class to conform toGathering
?*Holder
- Class-specific holders (explained later)CustomDataHolder
- Simplified holder (explained later)Signature examples
As an example, the RMC method
SecureConnection::RegisterEx
has the signatureCallRegisterEx__Q3_2nn3nex30SecureConnectionProtocolClientFPQ3_2nn3nex19ProtocolCallContextPQ3_2nn3nex7qResultRCQ3_2nn3nex36qList__tm__23_Q3_2nn3nex10StationURLPUiPQ3_2nn3nex10StationURLRCQ3_2nn3nex56AnyObjectHolder__tm__33_Q3_2nn3nex4DataQ3_2nn3nex6String
. The important part here being2nn3nex56AnyObjectHolder__tm__33_Q3_2nn3nex4DataQ3_2nn3nex6String
, which decodes tonn::nex::AnyObjectHolder<nn::nex::Data, nn::nex::String>
and represents thehCustomData
field of the requestHowever in other protocols, this is not the case. For example the RMC method
MatchmakeExtension::BrowseMatchmakeSession
has the signatureBrowseMatchmakeSession__Q3_2nn3nex24MatchmakeExtensionClientFPQ3_2nn3nex19ProtocolCallContextRCQ3_2nn3nex30MatchmakeSessionSearchCriteriaRCQ3_2nn3nex11ResultRangePQ3_2nn3nex87qList__tm__74_Q3_2nn3nex61AnyObjectHolder__tm__38_Q3_2nn3nex9GatheringQ3_2nn3nex6StringPQ3_2nn3nex39qList__tm__26_Q3_2nn3nex13GatheringURLs
. The important part here being2nn3nex61AnyObjectHolder__tm__38_Q3_2nn3nex9GatheringQ3_2nn3nex6String
, which decodes tonn::nex::AnyObjectHolder<nn::nex::Gathering, nn::nex::String>
and represents thehCustomData
field of the request and represents thelstGathering
field of the responseThis means that the current documentation and implementation is wrong,
AnyDataHolder
does NOT exist and it does NOT hold types likeGathering
. Instead a base template class calledAnyObjectHolder
is used to create these type holders. The confusion likely came about because the DDLs just refer to this asany
, so Kinnay likely just assumed thatany
was all the same data typeAdapter classes
These seem to just be part of standard adapter design, based on the class names and some of the functions? Unsure what they're ACTUALLY used for in NEX but they show up in signatures. Not super relevant for us I think
*Holder
classesTo expand on this further, @DaniElectra later discovered some debug logs still present in some games which give us more hints as to how this works. The debug logs are used to warn developers that an "obsolete" RMC method is being used:
This means that the original implementation seems to have expanded on
AnyObjectHolder
for more specific classes. In this caseGatheringHolder
which holds any class that inherits fromGathering
. We can use this to assume that in places likeSecureConnection::RegisterEx
a class calledDataHolder
is used, which matches up more closely to the original documentation2nd "length" field
@DaniElectra also discovered that the documentation for the structure is slightly wrong. The "second length" field IS part of a
Buffer
class. We implement it like this already for convenience, but the documentation is wrong and we have a comment saying the implementation is wrong:I later then checked the corresponding
StreamIn
method to confirm this:(Side note: This seems to confirm something I had previously thought to be the case, that
PersistentGathering
andCommunity
are interchangeable concepts. Nintendo probably changed the name at some point, but they're the exact same thing internally)CustomDataHolder
I later discovered
CustomDataHolder
, which seems to be a simplified abstraction on these holders. Where the holders previously could take in a 2nd class type,CustomDataHolder
only takes in the class type it is intended to be holding. This class builds offDataHolder
and always sets the 2nd class type toString
(AnyObjectHolder<nn::nex::Data, nn::nex::String>
), which means noCustomDataHolder
could ever have a different "identifier" typeThe
Messaging
protocol makes use of this to store messages (nn::nex::CustomDataHolder<nn::nex::UserMessage>
.UserMessage
inherits fromData
which makes it compatible withDataHolder
and thus usable inCustomDataHolder
, and bothTextMessage
andBinaryMessage
inherit fromUserMessage
). This is used, for example, inMessaging::DeliverMessage
as theoUserMessage
field of the requestIt seems like for all intents and purposes,
CustomDataHolder
andDataHolder
function identically. They just seem to get compiled differently and thus their signatures appear different in functions? However more research into this needs to be done to be sureAny other details to share? (OPTIONAL)
It looks like at some point maybe Kinnay noticed this as well and changed his common types (https://github.com/kinnay/NintendoClients/blob/e2b673bac6d7781e83f83ae2c5d0c34a2092d72e/nintendo/nex/common.py#L121-L155)? However this only seems to partially account for
DataHolder
and nothing else mentioned hereAn implementation of this in Go has been partially drafted, but nothing has been set in stone and all implementations require some level of hacks/compromises to work. All issues stem from the fact that Go does not have inheritance, and thus either types can be much too strict (which breaks cases like child classes) or too permissive (which prevents strict narrowing). A version of narrowing WITH child class support CAN be achieved by abusing interfaces however, but it's somewhat hacky
Below are the draft implementations as they evolved. These are provided for historical purposes, to see what has already been tried. I am not 100% sold on these, as they are somewhat hacky, so I am open to further design suggestions. However this may be as good as we are going to get:
8/13/24 (no child structs)
My first draft which allowed for type narrowing, but disallowed child structs:
8/13/24 ("identification struct method")
My second draft which allowed for both child structs and type narrowing, at the cost of
interface
abuse. This method uses the concept of a "identification struct method" on aninterface
to "trick" Go into allowing types which inherit from each other. This concept (hack, really) continues in all further implementations:8/18/24 (adds in the "identification" type support)
Dani later modified my 2nd draft to add in support for the "identifier" type:
8/18/24 ("HeldObject")
This is a modified version of Dani's implementation I made which adds in the concept of
HeldObject
to try and help further centralize and narrow our types. However this has the issue of making the "identification struct method" private. This was done to prevent types from outside the package from being valid, but this design prevents expansion so it should probably be changed if used:8/18/24 (CustomDataHolder)
Below is the first draft of an implementation of
CustomDataHolder
, building off the previous designs:@DaniElectra then later suggested a modified
New
function:The text was updated successfully, but these errors were encountered: