Jump to content

Recommended Posts

Posted

It's a long time coming, but it's finally here -- as of Forge 1.21.1, we've now implemented the de-facto common tags!

With the introduction of the de-facto common tags in Forge, there are now numerous tags that are shared across all major mod loaders. This means greater compatibility between mods across different loaders and a huge selection of new tags now bundled with Forge that are available for use in mods. It also means that datapack authors are less likely to need duplicate data or even separate datapacks for different loaders.

We wanted to make it worth the wait, so we've gone above and beyond to ensure that the implementation of the common tags in Forge is as comprehensive and seamless as possible.

Not only have we implemented all the tags -- including accounting for the many changes and additions made overtime -- we've also written tools that dump tags between the different loaders to identify differences, have comprehensive documentation on how to migrate to the new tags, bouncer fields to automatically make some old code use the new tags, avoided breaking changes entirely and adopted a stricter policy that clearly distinguishes between loader-specific and common tags.

If you see a `c:` tag definition in Forge for a given version, you can be confident that it'll always work on all loaders that support the common tags. Many Neo-specific/Fabric-specific `c:` tags have also been added to Forge, under the `forge:` namespace, but we'll actively migrate them to `c:` as soon as they're adopted by the other loaders and provide a graceful deprecation period for the old tags.

Overtime, we'll be working with other loaders to help further improve parity and keep things more in sync across versions moving forward, as well as updating Forge as new tags are added to other loaders, of course. We hope it has been worth the wait!

Developers can find more information on Forge's implementation, including the common tags dumper and migration guides, in the description of pull request 9955.

 

Background

As for that wait, why did it take so long to implement in Forge after the other loaders? Shouldn't it have been a simple copy-and-paste job?

Well... there were many hurdles to overcome that made implementing this feature in Forge difficult. A lot of work went into this, and I'd like to share just some of the many challenges faced.

 

First of which was documentation and ease of migration:

Early experiments of implementing this on Forge involved a simple copy-and-paste from Neo, however it quickly came apparent that this was not a good fit for Forge. There were many deliberate breaking changes that were initially undocumented, with various `forge:` namespaced tags being removed with no `c:` equivalents, around a hundred Neo-specific `c:` tags that were not available on Fabric despite having a dedicated `neoforge:` namespace for loader-specific tags and a "tag convention warning" system that would recommend migrating to the wrong tags as well as containing migration definitions from non-existent tags to `c:` equivalents and vice-versa.

The discussions as to why certain tags were added, renamed, added, changed or removed were done across multiple PRs, Discord servers and channels. I'm aware of many different Discord channels where the tags were discussed, spanning multiple servers and thousands of messages, not to mention over 40 separate PRs made to the two loaders after the initial PRs and their associated filed issues, some of which contained further breaking changes. With no centralised discussion or documentation, combined with both loaders having differing goals that led to loader-specific `c:` tags on both sides, as well as bugs in the automated warning systems, it was very challenging to find the correct mappings and understand the reasoning behind them.

 

Second was the approach taken by the other loaders:

My understanding is that there were multiple attempts to create a central repo containing the code for common tags that the loaders could pull from so that it would be easy to update the tags in one place and have them propagate to all loaders, with clear indicators of what tags are available for a given version as well as well-documented parity across loaders, which is exactly what Forge was asking for in the past. However, all these attempts fell through over disagreements on management and permissions.

As a last-ditch effort, the main person organising these discussions ended up directly creating PRs to the two loaders, but excluded Forge. These two initial big PRs were done accounting for loader-specific requests and with the intent of getting at least something done, rather than throwing away all the work that had been done so far. Bizarrely, the start of the description of both PRs implies that Forge was not interested in supporting the new tags, despite multiple Forge team members publicly expressing the opposite.

Other loaders had the luxury of having the PRs made for them, which they could then review and merge, while Forge was left out of the loop. To make matters worse, said main person also refused to review my work in progress implementation to ensure consistency across loaders, stating that he doesn't use Forge anymore. I was essentially left to figure out the whole thing single-handedly, cross-referencing thousands of comments across multiple places, accounting for loader-specific differences and trying to get it up to Forge's standards.

While working on Forge's implementation, people expressed frustration over this approach, as it looked like a deliberate attempt to make it hard for Forge to adopt the new tags. Even without considering the possibility of malice, it would've been easier to keep track of changes and loader-specific differences if a centralised document was maintained after loaders accept changes (that way the document would not require agreement from loaders). In my opinion, the manual approach of PRs to both sides and manually implementing parity improvements as they were requested without keeping track anywhere is more error-prone and labour-intensive.

The following day I received an stern DM accusing me of spreading conspiracies about him making it hard for Forge to implement the new tags, along with a brief explanation of why the centralised repo didn't happen which further shaped my understanding explained here. I took the opportunity to thank him for reaching out with an explanation and provided more context on the frustrations people had with the approach taken, highlighting things such as the spread out nature of discussions, the lack of clear documentation and his refusal to review my implementation. I offered to work with him to fix some of the bugs I found on other loaders and we came to an understanding. This turned out to be pretty beneficial as I was able to directly ask him about the tags, get updates on new follow-up PRs made to other loaders and made him aware of some bugs and mistakes with the warning system and tags. I'm thankful for his willingness to reach out and help.

 

Third is the sheer amount of tags and the moving target:

Due to the approach taken by other loaders as explained earlier, there is no versioning for the de-facto spec. This has its own benefits and drawbacks. The main drawback being that the tags are out of sync across Minecraft versions -- you may see some tags on both loaders for the latest version, but only on one loader when going back an MC version. This is due to the differences in approach between the two loaders, where one is more focused on the latest version while the other supports multiple versions. This isn't entirely without its benefits though, as it allows Neo to deliver new tags faster by not needing to worry about older versions.

There are many tags added, across many categories. Some existing Forge tags gained new contents, too. This is the biggest collaborative tag update the Minecraft modding community has ever seen, with a wide-reaching impact in terms of tags, which is a big win for interoperability and closely aligns with Forge's goal of being a compatibility layer for mods.

Since the first big two PRs were made and Forge started working on its implementation, I've been keeping track of many amendment PRs made, cross-referencing them with each loader and Forge itself to ensure parity. A lot of new things have been further added and parity has been improving, but this does mean it's a moving target. Catching up with something that's continuously evolving means you need to be quick but thorough, which is a difficult balance to strike.

While I've mostly been doing this single-handedly, I'd like to thank the people who have helped me along the way, such as Jonathan, Lex, TelepathicGrunt and others who have worked with me to overcome all of these hurdles and finally deliver Forge's implementation of the de-facto common tags.

 

Conclusion

I hope this document has provided some insight into some of the challenges faced when implementing the de-facto common tags in Forge, as well as the benefits it brings to the wider MC modding community. Now that all major mod loaders have adopted the common tags, I'm looking forward to seeing players, datapack authors and mod devs alike benefit from the compatibility benefits it brings to the whole MC modding ecosystem, no matter which loader you use.

Announcements



  • Recently Browsing

    • No registered users viewing this page.
  • Posts

    • Same problem when removing Sodium Embeddium and oculus __________ The game crashed whilst ticking entity Error: java.lang.NullPointerException: Cannot invoke "net.minecraft.client.player.LocalPlayer.getCapability(net.minecraftforge.common.capabilities.Capability)" because "net.minecraft.client.Minecraft.m_91087_().f_91074_" is null
    • https://privatebin.net/?0b840573a8db7fcb#8DPu21Wo8wdqDU4nNm1rTrv4GY7qpfDGR87zyWHiNLpB
    • I am writing a code that stores String List in a player capability. And I need to sync it to client so I could access it via Minecraft.getMinecraft.player. I use messages for that and everything works fine until I work with the PlayerClone event. According to the logs it should work just fine but for some reason it just doesn't. You need to reconnect to the world to resync everything (messages do work when I use it in PlayerLoggedInEvent). Here is my code:   CapabilitySync.java public class CapabilitySync { @SubscribeEvent public void onPlayerLogsIn(PlayerLoggedInEvent event) { EntityPlayer player = event.player; //That thing works just fine NetworkHandler.channel.sendTo(new ServerToClient(player), (EntityPlayerMP) player); } @SubscribeEvent public void onPlayerClone(PlayerEvent.Clone event) { EntityPlayer player = event.getEntityPlayer(); IFolder folder = player.getCapability(FolderProvider.FOLDER_CAP, null); IFolder oldfolder = event.getOriginal().getCapability(FolderProvider.FOLDER_CAP, null); folder.setFolders(oldfolder.getFolders()); // This part doesn't work NetworkHandler.channel.sendTo(new ServerToClient(player), (EntityPlayerMP) player); } } NetworkHandler.java public class NetworkHandler { public static SimpleNetworkWrapper channel = NetworkRegistry.INSTANCE.newSimpleChannel(Reference.MODID); public static void init() { channel.registerMessage(ClientToServer.Handler.class, ClientToServer.class, 0, Side.SERVER); channel.registerMessage(ServerToClient.Handler.class, ServerToClient.class, 1, Side.CLIENT); } public static IThreadListener getThreadListener(MessageContext ctx) { return ctx.side == Side.SERVER ? (WorldServer) ctx.getServerHandler().player.world : getClientThreadListener(); } @SideOnly(Side.CLIENT) public static IThreadListener getClientThreadListener() { return Minecraft.getMinecraft(); } } ClientToServer / ServerToClient messages public class ClientToServer implements IMessage { private List<String> folders; private int folders_count; public ClientToServer () {} public ClientToServer (IFolder folder) { this.folders = folder.getFolders(); this.folders_count = folder.size(); } @Override public void fromBytes(ByteBuf buf) { folders = new ArrayList<>(); folders_count = buf.readInt(); for (int i = 0; i < folders_count; i++) { folders.add(ByteBufUtils.readUTF8String(buf)); } } @Override public void toBytes(ByteBuf buf) { buf.writeInt(folders_count); for (int i = 0; i < folders_count; i++) { ByteBufUtils.writeUTF8String(buf, folders.get(i)); } } public List<String> getFolders (){ return this.folders; } public static class Handler implements IMessageHandler<ClientToServer, IMessage> { @Override public IMessage onMessage(ClientToServer message, MessageContext ctx) { EntityPlayerMP serverPlayer = ctx.getServerHandler().player; NetworkHandler.getThreadListener(ctx).addScheduledTask(() -> { IFolder old_folders = serverPlayer.getCapability(FolderProvider.FOLDER_CAP, null); List<String> new_folders = message.getFolders(); old_folders.setFolders(new_folders); }); return null; } } } public class ServerToClient implements IMessage { private List<String> folders; private int folders_count; public ServerToClient() {} public ServerToClient(EntityPlayer server_player) { this.folders = server_player.getCapability(FolderProvider.FOLDER_CAP, null).getFolders(); this.folders_count = folders.size(); } @Override public void fromBytes(ByteBuf buf) { folders = new ArrayList<>(); folders_count = buf.readInt(); for (int i = 0; i < folders_count; i++) { folders.add(ByteBufUtils.readUTF8String(buf)); } } @Override public void toBytes(ByteBuf buf) { buf.writeInt(folders_count); for (int i = 0; i < folders_count; i++) { ByteBufUtils.writeUTF8String(buf, folders.get(i)); } } public List<String> getFolders (){ return this.folders; } public static class Handler implements IMessageHandler<ServerToClient, IMessage> { @Override public IMessage onMessage(ServerToClient message, MessageContext ctx) { NetworkHandler.getThreadListener(ctx).addScheduledTask(() -> { Minecraft mc = Minecraft.getMinecraft(); IFolder old_folders = mc.player.getCapability(FolderProvider.FOLDER_CAP, null); List<String> new_folders = message.getFolders(); old_folders.setFolders(new_folders); }); return null; } } ClientProxy.java (where I access the capability through a client player) public class ClientProxy extends CommonProxy { @SubscribeEvent public void onKeyInput(KeyInputEvent event) { if (Keybinds.KEY_u.isPressed()) { EntityPlayer playerSP = Minecraft.getMinecraft().player; IFolder folder = playerSP.getCapability(FolderProvider.FOLDER_CAP, null); folder.add("UUUUU"); NetworkHandler.channel.sendToServer(new ClientToServer(folder)); } else if (Keybinds.KEY_i.isPressed()) { EntityPlayer playerSP = Minecraft.getMinecraft().player; IFolder folder = playerSP.getCapability(FolderProvider.FOLDER_CAP, null); for (String f : folder.getFolders()) { String message = f; playerSP.sendMessage(new TextComponentString(message)); } }else if (Keybinds.KEY_o.isPressed()) { EntityPlayer playerSP = Minecraft.getMinecraft().player; IFolder folder = playerSP.getCapability(FolderProvider.FOLDER_CAP, null); Minecraft.getMinecraft().displayGuiScreen(new DefaultGUI(folder)); } } }  
  • Topics

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.