• Register
    • Login
    • Search
    • Recent
    • Tags
    • Popular
    • Users
    • Groups

    Solved GUI/GuiHandler/Container/Inventory pour une Entity

    1.7.x
    1.7.10
    2
    7
    1256
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Kujaroth
      Kujaroth last edited by

      Salut salut,

      J’essaie de faire un GUI pour mon entity, le soucis c’est que j’en ai jamais fait, j’ai coder quelques trucs en suivant divers tuto sur block et item (rien trouver pour une entité).

      Le soucis c’est que, d’une part, aucune idée si mes code sont correcte et d’autre part, j’ai un soucis sur mon GuiHandler avec des constructeur indefinie, j’ai beau essayer de changer, j’ai toujours des problemes…

      GuiHandler

      public class GuiHandlerClapTrap implements IGuiHandler {
      
          @Override
             public Object getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z)
          {
      
                 if(ID == 0)
                 {
      
                     EntityClapTrap entity = (EntityClapTrap)world.getEntityByID(x); // on recup l'entité
      // erreur ci dessous
                     return new ContainerClapTrap(player.inventory, world, entity);     //< ici
      //
                             }
                 return null;
             }
      
             @Override
             public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z)
             {
                 if(ID == 0)
                 {
                     EntityClapTrap entity = (EntityClapTrap)world.getEntityByID(x); // on recup l'entité
      // erreur ci dessous
                     return new GuiClapTrap(player.inventory, world, entity);  // < ici
      //
      
                 }
                 return null;
             }
          }
      
      

      Voici mes autres classe, il n’y a aucune erreur via eclipse, mais y’en a surement coté codage…

      Container

       private final InventoryClapTrap tileTuto;
      
          public ContainerClapTrap(InventoryClapTrap tile, InventoryPlayer inventory)
          {
              this.tileTuto = tile;
              inventory.openInventory();
              for(int i = 0; i < 3; ++i)
              {
                  for(int j = 0; j < 9; ++j)
                  {
                      this.addSlotToContainer(new Slot(inventory, j + i * 9, 8 + j * 18, 18 + i * 18));
                  }
              }
             // this.bindPlayerInventory(entity);
          }
      
          private void bindPlayerInventory(InventoryClapTrap entity)
          {
              int i;
              for(i = 0; i < 3; ++i)
              {
                  for(int j = 0; j < 9; ++j)
                  {
                      this.addSlotToContainer(new Slot(entity, j + i * 9 + 9, 8 + j * 18, 86 + i * 18));
                  }
              }
      
              for(i = 0; i < 9; ++i)
              {
                  this.addSlotToContainer(new Slot(entity, i, 8 + i * 18, 144));
              }
          }
      
          public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex)
          {
              ItemStack itemstack = null;
              Slot slot = (Slot)this.inventorySlots.get(slotIndex);
      
              if(slot != null && slot.getHasStack())
              {
                  ItemStack itemstack1 = slot.getStack();
                  itemstack = itemstack1.copy();
      
                  if(slotIndex < this.tileTuto.getSizeInventory())
                  {
                      if(!this.mergeItemStack(itemstack1, this.tileTuto.getSizeInventory(), this.inventorySlots.size(), true))
                      {
                          return null;
                      }
                  }
                  else if(!this.mergeItemStack(itemstack1, 0, this.tileTuto.getSizeInventory(), false))
                  {
                      return null;
                  }
      
                  if(itemstack1.stackSize == 0)
                  {
                      slot.putStack((ItemStack)null);
                  }
                  else
                  {
                      slot.onSlotChanged();
                  }
              }
              return itemstack;
          }
      
          @Override
          public boolean canInteractWith(EntityPlayer player)
          {
              return this.tileTuto.isUseableByPlayer(player);
          }
      
          public void onContainerClosed(EntityPlayer player)
          {
              super.onContainerClosed(player);
              this.tileTuto.closeInventory();
          }
      }
      

      Gui

      public class GuiClapTrap extends GuiContainer
      {
          private static final ResourceLocation textures = new ResourceLocation(ModClapTrap.MODID, "textures/gui/container/inventory_clap.png");
          private InventoryClapTrap tileTuto;
          private IInventory playerInv;
      
          public GuiClapTrap(InventoryClapTrap tile, InventoryPlayer inventory)
          {
              super(new ContainerClapTrap(tile, inventory));
              this.tileTuto = tile;
              this.playerInv = inventory;
              this.allowUserInput = false;
              this.ySize = 170;
          }
      
          protected void drawGuiContainerForegroundLayer(int x, int y)
          {
              String tileName = this.tileTuto.hasCustomInventoryName() ? this.tileTuto.getInventoryName() : I18n.format(this.tileTuto.getInventoryName());
              this.fontRendererObj.drawString(tileName, this.xSize / 2 - this.fontRendererObj.getStringWidth(tileName) / 2, 6, 0);
              String invName = this.playerInv.hasCustomInventoryName() ? this.playerInv.getInventoryName() : I18n.format(this.playerInv.getInventoryName());
              this.fontRendererObj.drawString(invName, (this.xSize - this.fontRendererObj.getStringWidth(invName)) / 2, this.ySize - 96, 0);
          }
      
          @Override
          protected void drawGuiContainerBackgroundLayer(float partialRenderTick, int x, int y)
          {
              GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
              this.mc.getTextureManager().bindTexture(textures);
              int k = (this.width - this.xSize) / 2;
              int l = (this.height - this.ySize) / 2;
              this.drawTexturedModalRect(k, l, 0, 0, this.xSize, this.ySize);
      
              /*
              this.drawTexturedModalRect(k, l, 0, 0, this.xSize, this.inventoryRows * 18 + 17);
              this.drawTexturedModalRect(k, l + this.inventoryRows * 18 + 17, 0, 126, this.xSize, 96);
              */
          }
      }
      
      

      Inventory(tile entity ?)

      public class InventoryClapTrap implements IInventory
      {
          public ItemStack[] content;
          public int size;
      
          public InventoryClapTrap(ItemStack container, int size) {
              this.size = size;
              this.content = new ItemStack;
              if (!container.hasTagCompound()) container.setTagCompound(new NBTTagCompound());
              this.readFromNBT(container.getTagCompound());
          }
      
          /**
           * This methods reads the content of the NBTTagCompound inside the container
           *
           * @param comp
           *            the container NBTTagCompound
           */
          public void readFromNBT(NBTTagCompound comp) {
              NBTTagList nbtlist = comp.getTagList("Inventory", Constants.NBT.TAG_COMPOUND);
              for (int i = 0; i < nbtlist.tagCount(); i++) {
                  NBTTagCompound comp1 = nbtlist.getCompoundTagAt(i);
                  int slot = comp1.getInteger("Slot");
                  this.content[slot] = ItemStack.loadItemStackFromNBT(comp1);
              }
          }
      
          /**
           * This methods saves the content inside the container
           *
           * @param comp
           *            the NBTTagCompound to write in
           */
          public void writeToNBT(NBTTagCompound comp) {
              NBTTagList nbtlist = new NBTTagList();
      
              for (int i = 0; i < this.size; i++) {
                  if (this.content* != null) {
                      NBTTagCompound comp1 = new NBTTagCompound();
                      comp1.setInteger("Slot", i);
                      this.content*.writeToNBT(comp1);
                      nbtlist.appendTag(comp1);
                  }
              }
              comp.setTag("Inventory", nbtlist);
          }
      
          @Override
          public int getSizeInventory() {
              return this.size;
          }
      
          @Override
          public ItemStack getStackInSlot(int index) {
              return this.content[index];
          }
      
          @Override
          public ItemStack decrStackSize(int index, int amount) {
              ItemStack stack = getStackInSlot(index);
              if (stack != null) {
                  if (stack.stackSize > amount) {
                      stack = stack.splitStack(amount);
                      if (stack.stackSize == 0) this.content[index] = null;
                  } else {
                      this.content[index] = null;
                  }
              }
              return stack;
          }
      
          @Override
          public ItemStack getStackInSlotOnClosing(int index) {
              ItemStack stack = getStackInSlot(index);
              if (stack != null) this.content[index] = null;
              return stack;
          }
      
          @Override
          public void setInventorySlotContents(int index, ItemStack stack) {
              this.content[index] = stack;
          }
      
          @Override
          public String getInventoryName() {
              return ModClapTrap.MODID + ".container.inventory_clap";
          }
      
          @Override
          public boolean hasCustomInventoryName() {
              return false;
          }
      
          @Override
          public int getInventoryStackLimit() {
              return 64;
          }
      
          @Override
          public void markDirty() {}
      
          @Override
          public boolean isUseableByPlayer(EntityPlayer player) {
              return true;
          }
      
          @Override
          public void openInventory() {}
      
          @Override
          public void closeInventory() {}
      
          /**
           * Prevents backpack-ception
           */
          @Override
          public boolean isItemValidForSlot(int index, ItemStack stack) {
              return true;
          }
      
      }
      
      

      Je m’excuse par avance des énorme boulette présente… Un tuto pour mettre un GUI sur une entity serai le bienvenue 😉

      Cordialement.

      >! Bonjour/Bonsoir,
      >! Vous désirez un Modèle ou une Texture pour votre Mod ? Vous pouvez faire un…

      1 Reply Last reply Reply Quote 0
      • robin4002
        robin4002 Moddeurs confirmés Rédacteurs Administrateurs last edited by

        Salut,
        C’est pas juste un gui, c’est un container, un inventaire et un gui. Il faut bien faire la différence. Juste un gui ce n’est pas la même chose que les 3.
        Ton entité devrait avoir l’interface IInventory Et tu devrais utiliser les fonctions en rapport avec les nbt de l’entité pour sauvegarder l’inventaire.

        Par contre c’est bien joli de dire il y a une erreur ici, mais si tu ne précise pas quelle est l’erreur ça va être difficile de t’aider.
        Je suis un programmeur, pas un magicien.

        1 Reply Last reply Reply Quote 0
        • Kujaroth
          Kujaroth last edited by

          Constructeur indéfinie, pour la classe Gui et Container.

          The constructor ContainerClapTrap(InventoryPlayer, World, EntityClapTrap) is undefined (ligne 20)
          The constructor GuiClapTrap(InventoryPlayer, World, EntityClapTrap) is undefined (ligne 31)

          Sûr que c’est un truc bidon, mais je tourne en rond ><

          C’est pas juste un gui, c’est un container, un inventaire et un gui. Il faut bien faire la différence. Juste un gui ce n’est pas la même chose que les 3.

          En effet, j’ai été trop évasif, je vais éditer mon titre.

          Voilà, je suis bloquer ici… Pour les NBT je devrais pouvoir me débrouiller (je l’esper) une fois qu’il n’y aura plus d’erreur et que le GUI s’ouvrira.

          Cordialement.

          EDIT : Bon, en faite c’était juste que j’avais bien fait n’importe quoi… Tout marche impec’ sauf… Que tout mes Clap on le même inventaire. A m’on avis c’est un soucis d’ID, la je prend les clap en général ><

          Je n’arrive pas a concorder

          EntityClapTrap entity = (EntityClapTrap)world.getEntityByID(x); < ça
          return new ContainerClapTrap(player.inventory, new InventoryClapTrap(player.getHeldItem(), 54)); < avec ça

          je ne peut pas mettre player.inventory, world, entity
          car les constructeur de la classe Container ne correspond plus, et si je change, tout foire… (constructeur : InventoryPlayer playerInv, InventoryClapTrap inv)

          >! Bonjour/Bonsoir,
          >! Vous désirez un Modèle ou une Texture pour votre Mod ? Vous pouvez faire un…

          1 Reply Last reply Reply Quote 0
          • robin4002
            robin4002 Moddeurs confirmés Rédacteurs Administrateurs last edited by

            Bha oui car dans la constructeur de ta classe InventoryClapTrap est :
            public InventoryClapTrap(ItemStack container, int size) {
            C’est à toi de changer ça …
            Pareil pour le constructeur de ContainerClapTrap et le constructeur de GuiClapTrap.

            1 Reply Last reply Reply Quote 0
            • Kujaroth
              Kujaroth last edited by

              Désolé si ça a l’air si évidant pour toi…

              Je dois changer le constructeur InventoryClapTrap… Ok… Mais en quoi ? Je ne vois rien a part ItemStack et slot, c’est un inventaire.

              Et comme je l’ai déjà dit, je n’arrive pas a faire correspondre les constructeur, j’ai beau mettre ce qu’il manque comme le World world, j’ai toujours des erreurs…

              De plus, je trouve quedal sur le net ><

              :::

              GuiHandler

              import cpw.mods.fml.common.network.IGuiHandler;
              import net.minecraft.entity.player.EntityPlayer;
              import net.minecraft.world.World;
              
              public class GuiHandler implements IGuiHandler
              {
                  @Override
                  public Object getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) {
                      switch (ID) {
                      case 0:
                          // The last parameter must be a multiple of 9 (e.g: 9, 18, 27, 54)
                          // Condition to check if the player has the right item in hand
              
                          EntityClapTrap entity = (EntityClapTrap)world.getEntityByID(x);
                          return new ContainerClapTrap(player.inventory, world, entity);
              
                          }
                      return null;
                  }
              
                  @Override
                  public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) {
                      switch (ID) {
                      case 0:
                          // The last parameter must be a multiple of 9 (e.g: 9, 18, 27, 54)
                          // Condition to check if the player has the right item in hand
              
                          EntityClapTrap entity = (EntityClapTrap)world.getEntityByID(x);
                          return new GuiClapTrap(player.inventory, world, entity);
                      }
                      return null;
                  }
              }
              

              ContainerClapTrap

              public class ContainerClapTrap extends Container
              {
                  public InventoryClapTrap invClap;
                  public int rows;
              
                  public ContainerClapTrap(InventoryPlayer playerInv, InventoryClapTrap inv) {
                      this.invClap = inv;
                      this.rows = inv.getSizeInventory() / 9;
                      int i = (this.rows - 4) * 18;
                      int j;
                      int k;
              
                      // Adding slots to the backpack
                      for (j = 0; j < this.rows; ++j) {
                          for (k = 0; k < 9; ++k) {
                              this.addSlotToContainer(new SlotBackPack(inv, k + j * 9, 8 + k * 18, 18 + j * 18));
                          }
                      }
              
                      // Adding player's slots
                      for (j = 0; j < 3; ++j) {
                          for (k = 0; k < 9; ++k) {
                              this.addSlotToContainer(new Slot(playerInv, k + j * 9 + 9, 8 + k * 18, 103 + j * 18 + i));
                          }
                      }
              
                      for (j = 0; j < 9; ++j) {
                          this.addSlotToContainer(new Slot(playerInv, j, 8 + j * 18, 161 + i));
                      }
                  }
              
                  @Override
                  public boolean canInteractWith(EntityPlayer player) {
                      return true;
                  }
              
                  public void writeToNBT(ItemStack stack) {
                      if (!stack.hasTagCompound()) stack.setTagCompound(new NBTTagCompound());
                      invClap.writeToNBT(stack.getTagCompound());
                  }
              
                  @Override
                  public ItemStack transferStackInSlot(EntityPlayer player, int index) {
                      ItemStack itemstack = null;
                      Slot slot = (Slot) this.inventorySlots.get(index);
              
                      if (slot != null && slot.getHasStack()) {
                          ItemStack itemstack1 = slot.getStack();
                          itemstack = itemstack1.copy();
              
                          // Prevents backpack-ception (backpack inside backpack) with
                          // shift-click
              
                          if (index < this.invClap.getSizeInventory()) {
                              if (!this.mergeItemStack(itemstack1, this.invClap.getSizeInventory(), this.inventorySlots.size(), true)) return null;
                          } else if (!this.mergeItemStack(itemstack1, 0, this.invClap.getSizeInventory(), false)) { return null; }
              
                          if (itemstack1.stackSize == 0)
                              slot.putStack((ItemStack) null);
                          else
                              slot.onSlotChanged();
                      }
              
                      return itemstack;
                  }
              
                  /**
                   * @param buttonPressed
                   *            left click, right click, wheel click, etc.
                   * @param flag
                   *            category (e.g.: hotbar keys)
                   */
                  @Override
                  public ItemStack slotClick(int slotIndex, int buttonPressed, int flag, EntityPlayer player) {
                      // Prevents from removing current backpack
                      if (flag == 2 && buttonPressed == player.inventory.currentItem) return null;
                      if (slotIndex - this.invClap.getSizeInventory() - 27 == player.inventory.currentItem) return null;
                      return super.slotClick(slotIndex, buttonPressed, flag, player);
                  }
              
                  /**
                   * Used to save content
                   */
                  @Override
                  public void onContainerClosed(EntityPlayer player) {
                      this.writeToNBT(player.getHeldItem());
                      super.onContainerClosed(player);
                  }
              }
              

              GuiClapTrap

              public class GuiClapTrap extends GuiContainer
              {
                  public static final ResourceLocation texture = new ResourceLocation("claptrapmod","textures/gui/container/inventory_clap.png");
                  protected InventoryClapTrap invClap;
                  protected InventoryPlayer playerInv;
                  public int rows;
              
                  public GuiClapTrap(InventoryPlayer playerInv, World world, InventoryClapTrap invClap) {
                      super(new ContainerClapTrap(playerInv, invClap));
                      this.playerInv = playerInv;
                      this.invClap = invClap;
                      this.allowUserInput = false;
                      // Calculate the number of rows
                      this.rows = invClap.getSizeInventory() / 9;
                      // Height of the GUI using the number of rows
                      this.ySize = 114 + this.rows * 18;
                  }
              
                  @Override
                  protected void drawGuiContainerForegroundLayer(int x, int y) {
                      this.fontRendererObj.drawString(I18n.format(this.invClap.getInventoryName(), new Object[0]), 8, 6, 4210752);
                      this.fontRendererObj.drawString(this.playerInv.hasCustomInventoryName() ? this.playerInv.getInventoryName() : I18n.format(this.playerInv.getInventoryName(), new Object[0]), 8, this.ySize - 96 + 2, 4210752);
                  }
              
                  @Override
                  protected void drawGuiContainerBackgroundLayer(float prt, int x, int y) {
                      GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
                      this.mc.getTextureManager().bindTexture(texture);
              
                      // Centering GUI
                      int k = (this.width - this.xSize) / 2;
                      int l = (this.height - this.ySize) / 2;
              
                      // Drawing the first part of the GUI (slots of the backpack)
                      this.drawTexturedModalRect(k, l, 0, 0, this.xSize, this.rows * 18 + 17);
                      // And after the slots from the player's inventory
                      this.drawTexturedModalRect(k, l + this.rows * 18 + 17, 0, 126, this.xSize, 96);
                  }
              }
              

              **
              InventoryClapTrap**

              public class InventoryClapTrap implements IInventory
              {
                  public ItemStack[] content;
                  public int size;
              
                  public InventoryClapTrap(ItemStack container, int size) {
                      this.size = size;
                      this.content = new ItemStack;
                      if (!container.hasTagCompound()) container.setTagCompound(new NBTTagCompound());
                      this.readFromNBT(container.getTagCompound());
                  }
              
                  /**
                   * This methods reads the content of the NBTTagCompound inside the container
                   *
                   * @param comp
                   *            the container NBTTagCompound
                   */
                  public void readFromNBT(NBTTagCompound comp) {
                      NBTTagList nbtlist = comp.getTagList("Inventory", Constants.NBT.TAG_COMPOUND);
                      for (int i = 0; i < nbtlist.tagCount(); i++) {
                          NBTTagCompound comp1 = nbtlist.getCompoundTagAt(i);
                          int slot = comp1.getInteger("Slot");
                          this.content[slot] = ItemStack.loadItemStackFromNBT(comp1);
                      }
                  }
              
                  /**
                   * This methods saves the content inside the container
                   *
                   * @param comp
                   *            the NBTTagCompound to write in
                   */
                  public void writeToNBT(NBTTagCompound comp) {
                      NBTTagList nbtlist = new NBTTagList();
              
                      for (int i = 0; i < this.size; i++) {
                          if (this.content* != null) {
                              NBTTagCompound comp1 = new NBTTagCompound();
                              comp1.setInteger("Slot", i);
                              this.content*.writeToNBT(comp1);
                              nbtlist.appendTag(comp1);
                          }
                      }
                      comp.setTag("Inventory", nbtlist);
                  }
              
                  @Override
                  public int getSizeInventory() {
                      return this.size;
                  }
              
                  @Override
                  public ItemStack getStackInSlot(int index) {
                      return this.content[index];
                  }
              
                  @Override
                  public ItemStack decrStackSize(int index, int amount) {
                      ItemStack stack = getStackInSlot(index);
                      if (stack != null) {
                          if (stack.stackSize > amount) {
                              stack = stack.splitStack(amount);
                              if (stack.stackSize == 0) this.content[index] = null;
                          } else {
                              this.content[index] = null;
                          }
                      }
                      return stack;
                  }
              
                  @Override
                  public ItemStack getStackInSlotOnClosing(int index) {
                      ItemStack stack = getStackInSlot(index);
                      if (stack != null) this.content[index] = null;
                      return stack;
                  }
              
                  @Override
                  public void setInventorySlotContents(int index, ItemStack stack) {
                      this.content[index] = stack;
                  }
              
                  @Override
                  public String getInventoryName() {
                      return ModClapTrap.MODID + ".container.inventoryclap";
                  }
              
                  @Override
                  public boolean hasCustomInventoryName() {
                      return false;
                  }
              
                  @Override
                  public int getInventoryStackLimit() {
                      return 64;
                  }
              
                  @Override
                  public void markDirty() {}
              
                  @Override
                  public boolean isUseableByPlayer(EntityPlayer player) {
                      return true;
                  }
              
                  @Override
                  public void openInventory() {}
              
                  @Override
                  public void closeInventory() {}
              
                  /**
                   * Prevents backpack-ception
                   */
              
                  @Override
                  public boolean isItemValidForSlot(int p_94041_1_, ItemStack p_94041_2_) {
                      // TODO Auto-generated method stub
                      return false;
                  }
              
              }
              

              :::

              Après, quoi dire… Oui, je suis une quiche, je n’ai jamais coder rien de tel, je ne sais pas où chercher et je ne comprend pas tout… C’est pas que je ne veux pas y donner du mien, comme si je balançais le truc et “allez, donner moi la réponse”. Je trifouille pendant des heures a me cassés la tête ><

              En plus, donner le code tout fait sans explications c’est pas très constructif.

              Donc bon… Merci d’usé de ton temps pour moi.

              Cordialement.

              >! Bonjour/Bonsoir,
              >! Vous désirez un Modèle ou une Texture pour votre Mod ? Vous pouvez faire un…

              1 Reply Last reply Reply Quote 0
              • robin4002
                robin4002 Moddeurs confirmés Rédacteurs Administrateurs last edited by

                Commence par appliquer ça :
                @‘robin4002’:

                Ton entité devrait avoir l’interface IInventory Et tu devrais utiliser les fonctions en rapport avec les nbt de l’entité pour sauvegarder l’inventaire.

                Donc la classe InventoryClapTrap tu la supprimes complètement et tu mets à la place tout dans l’entité. Ensuite tu verra surement les choses plus clairement.

                1 Reply Last reply Reply Quote 1
                • Kujaroth
                  Kujaroth last edited by

                  .
                  …
                  …

                  Bon… Que dire… Tu veux quel outil ? Un fouet ? Une batte en fer ? Une planche clouté ? Ou autre ?

                  Nah, car en faite… Je n’avais pas vue le "Ton entité devrait avoir l’interface IInventory ", juste ce qu’il y avais apres… Effectivement, une fois fait, j’y est vue plus clair, bien plus clair même… Mais c’est que je me suis tellement embrouiller et tout que… Bah voilà…

                  Je devine la “rage” ou le “désespoir” quand tu a me lisais. Merci d’être rester jusqu’au boue serieux.

                  Cordialement.

                  [résolue]

                  >! Bonjour/Bonsoir,
                  >! Vous désirez un Modèle ou une Texture pour votre Mod ? Vous pouvez faire un…

                  1 Reply Last reply Reply Quote 0
                  • 1 / 1
                  • First post
                    Last post
                  Design by Woryk
                  Contact / Mentions Légales

                  MINECRAFT FORGE FRANCE © 2018

                  Powered by NodeBB