-
-
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
Root creates an spurious Compound that is not in actual NBT data #145
Comments
The fix itself is quite simple: class Root(Compound):
__slots__ = (
'root_name',
)
# Optional, but safer than relying on .parse() to be always invoked
def __init__(self, root_name: str = "", *args, **kwargs):
super().__init__(*args, **kwargs)
self.root_name: str = root_name
# Delete all other properties and its setters
# or leave a temporary "DeprecationWarning":
@property
def root(self):
print("Root.root is deprecated, just access its contents directly")
return self
@classmethod
def parse(cls, buff, byteorder='big'):
tag_id = read_numeric(BYTE, buff, byteorder)
if not tag_id == cls.tag_id:
# Possible issues with a non-Compound root:
# - root_name might not be in __slots__(), and thus not assignable
# - not returning a superclass might be surprising and have side-effects
raise TypeError("Non-Compound root tags is not supported:"
f"{cls.get_tag(tag_id)}")
name = read_string(buff, byteorder)
self = super().parse(buff, byteorder)
self.root_name = name
return self
def write(self, buff, byteorder='big'):
write_numeric(BYTE, self.tag_id, buff, byteorder)
write_string(getattr(self, 'root_name', "") or "", buff, byteorder)
super().write(buff, byteorder) The biggest challenging in implementing this API breakage considerations. But if needed I'd happily provide a PR |
The documentation, at least at the time, was a bit fuzzy. It said that nbt files are implicitly always compound tags. In python since we have dictionaries nbtlib doesn't see nbt compounds as a collection of named tags, where each tag has an intrisic name, but rather as a proper dictionary where tags don't have any intrisic name. This makes the interpretation of the root compound ambiguous. Since in this implementation tags don't have any intrisic names, we can make the assumption that the file is an implicit compound in the sense that we can assume it's prefixed with the TAG_Compound marker. This makes the root name a key of the implicit compound. That's the reasoning that led to the way this is currently handled, but I know this just ends up adding unnecessary noise so I'd be happy to get rid of it. But yeah due to API breakage this would need to be published in a 2.0 release. |
While I'm still digesting your great points, and elaborating an extended answer, I've noticed you dropped And |
There are 2 important and distinct design choices involved here, both are open to interpretation by the "official" docs: 1 - Who "owns" a tag's name? Itself or its parent? For 1, some libraries, such as the old
|
But a tag not owning its own name leaves In
In my library (and in the example above), I did the latter. Can that name be saved at |
So handling a root's name while keeping
In practice, yes. And virtually all libraries assume so, in one way or the other. |
So, NBT-wise, |
In order to avoid the extra layer and preserve the root name, we must parse it separately from other tags, as each tag's
Problem is... a tag itself not only does not know its own name, it also does not write its own tag type on We need a way to save a tag, any tag, as root. That is, to write its own type and its own name, unlike other tags. So a root, conceptually, is not an entity by itself, but rather a trait. If we allow non-compounds as root, then "rootness" should be inherited by any tag. |
In this scenario, class
Life is then good and simple. But we're imposing a restriction on NBT. Spec implicitely says its fine, but technically the NBT data does allow more. |
Just had this crazy (or brilliant) idea: why not make
So, what do you think of this idea? |
I've just found this: http://web.archive.org/web/20110723210920/http://www.minecraft.net/docs/NBT.txt
Which I guess settles the question. No need for That said, can I make 2 requests?
|
In my mind it didn't make much sense to keep The idea of making a Of course the issue is that you could still call |
The useless (and potentially misleading)
I agree. Specially now that we have "official" docs saying it's always Compounds. Not only realistically, but per the spec. But still... oh my... so tempting...
Sweet! I'm listing...
... tag name and tag type too, don't forget it. Hum, I like it. Gives me a shiver when I see a tag handles its own name (
Ewww, noooo, no, no.
Thanks!!!! :-D (And if I fail to resist the temptation, I in the future extended Root with a meta like But I will resist this temptation. There are none non-compound roots in the wild. It's in the damn spec. It's useless. I'm better than that. |
Full disclaimer here: in the meantime since I've opened this 12 days ago, I've played with some ideas about the chunk/region relationship that are completely unrelated to this File/Root discussion. I'm trying to decouple *: well, not Chunks, but rather "[category][Chunk]", as of 1.17 we have region/, entities/ and poi/ subdirs. In this new model, it does make sense for a Chunk to have a This is all to say that, in the end, it is possible that my Chunks end up inheriting from But I still think they have a reason to exist, both conceptually and in my heart :) |
I've been silent as I've spent the last 10 weeks or so playing Minecraft instead of coding for it. Well, actually 10% playing and 90% creating mods and resourcepacks. Inevitably I stumbled on MCP and Minecraft's source code, and it gave many useful answers and insights:
|
I am seeing this error:
on a couple of player.dat files that I know are not corrupt. |
@Netherwhal , care to post some example files? I'm assuming they're empty players, correct? In any case, I believe this would be better handled as a separate issue |
@MestreLion / @vberlier - no those are proper nbt files that I can open without any issues with other tools. I am happy to pay a commission for this to be fixed in this library. |
@Netherwhal , what version are you using? I've tested latest 2.0.4 from Pypi and it handles your test case just fine: ~/minecraft/test $ venv
Cache entry deserialization failed, entry ignored
Collecting pip
Using cached https://files.pythonhosted.org/packages/08/e3/57d4c24a050aa0bcca46b2920bff40847db79535dc78141eb83581a52eb8/pip-23.1.2-py3-none-any.whl
Collecting setuptools
Using cached https://files.pythonhosted.org/packages/c7/42/be1c7bbdd83e1bfb160c94b9cafd8e25efc7400346cf7ccdbdb452c467fa/setuptools-68.0.0-py3-none-any.whl
Collecting wheel
Using cached https://files.pythonhosted.org/packages/61/86/cc8d1ff2ca31a312a25a708c891cf9facbad4eae493b3872638db6785eb5/wheel-0.40.0-py3-none-any.whl
Installing collected packages: pip, setuptools, wheel
Found existing installation: pip 9.0.1
Uninstalling pip-9.0.1:
Successfully uninstalled pip-9.0.1
Found existing installation: setuptools 39.0.1
Uninstalling setuptools-39.0.1:
Successfully uninstalled setuptools-39.0.1
Successfully installed pip-23.1.2 setuptools-68.0.0 wheel-0.40.0
(venv) ~/minecraft/test $ pip install nbtlib
Collecting nbtlib
Downloading nbtlib-2.0.4-py3-none-any.whl (28 kB)
Collecting numpy (from nbtlib)
Using cached numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.3 MB)
Installing collected packages: numpy, nbtlib
Successfully installed nbtlib-2.0.4 numpy-1.24.4
(venv) ~/minecraft/test $ python
Python 3.8.0 (default, Feb 28 2023, 16:22:29)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import nbtlib
>>> data = nbtlib.load('../mcworldlib/data/6d26bff4.dat')
>>> type(data)
<class 'nbtlib.nbt.File'>
>>> len(data)
55 Maybe this was fixed already by eda4d32 ? |
I've mentioned this a few years back, and now I'm familiar enough with NBT in binary form to raise this again, more confident that this is indeed a design bug in this awesome library.
Currently, when loading an NBT file like this (uncompressed for clarity):
We have this result:
It looks like we have a
Compound
as root, and then another (unnamed)Compound
inside it. But that's not true, the binary data clearly shows there is a single (unnamed)Compound
in the beginning. So the actual parsing result should be:Notice the root name is not represented in the case. And it does not matter, as a tag's name is parsed by (and belongs to) a tag's parent. A tag by itself has no idea about its own name (and tags in lists don't even have one).
Ok, the root tag of an NBT data does have a name, even if 99% (all?) of real world NBT have it empty. But having a name does not make it a Compound with a single child named as itself. This is wrong! If forces some weird syntax to acess the content:
Instead of a much simpler (and correct)
tag["DataVersion"]
If
Root
is aCompound
, it should not require extra syntax to access its contents. No other tag requires so. If preserving the root name is important for saving/loading integrity, it should be stored elsewhere (Root.name
perhaps?)If displaying this name whenever printing the root tag is needed (why would it be?), then I suggest this format:
This does not imply there are two nested compounds in the beginning. Much cleaner, easier to use, and correctly reflects the NBT data. You can even completely omit the name for empty names, and start right away with
{
(as my previous example)This format could also be used to display names for non-compound root tags too. A case that Minecraft seems not to use, and
nbtlib
does not support, but given the NBT spec there is no technical limitation:Anyway, allowing root to be a non-compound is a challenge for another day. But for now, removing the fake extra Compound would be very very nice!!!
The text was updated successfully, but these errors were encountered: