Jump to content

ChickenBones Multipart - [Highly Advanced]


Busti

Recommended Posts

ChickenBones Multipart is an API / Library by Chickenbones to create multiple Tileentitys inside a single block, so called Parts.

 

Note:

The mod is released under the name 'Forge' Multipart, however, it is NOT official and has no relation to the official Forge project. As such we will refer to it as CBMP in reference to it's author ChickenBones.

 

It also comes with a submod called 'Forge' Microblock (Again, no relation to Forge itself) which adds some parts to, for instance, cover up pipes. Some spotlights can be found on its official Minecraft Forums Topic. [Click Here]

This post will contain all CBMP related tutorials. If you want to make your own just post it as a reply and I am going to add it.

 

CBMP makes use of a programming language called Scala. Please make sure that you have basic knowledge of Scala since almost all of CBMP's code is written in scala.

On of the best tutorials for Scala is this one:

 


There's also a Java implementation of this tutorial by larsgerrits

https://github.com/Larsg310/FMP-Tutorial


Another great tutorial was made by Whov:

http://whov.altervista.org/forge-modding-tutorials/


 

All Tutorials in chronological order:

 

[spoiler=Installing Scala]

[*]Download and Install the latest Scala binaries from: http://www.scala-lang.org/download/

[*]IntelliJ Idea is the better choice because it suggests scala code.

  • Eclipse: (Please note that the scala plugin is not ready for Eclipse Luna yet)

     

    [*]IntelliJ Idea:

    [*]Go to: File / Settings / Plugins and click 'Install JetBrains Plugin...'

    [spoiler=Image]aUXdO.png

     

    [*]Choose "Scala" from the List and Install the plugin.

    [*]Right-click on your project and choose 'Add FrameWork support' from the context menu.

    [*]Select Scala in the list and correct the path for the scala home to where you installed your scala binaries. For me its C://Program File(x86)

     

     

    [*]You should now be able to use scala code and files.

     

     

     

    [spoiler=Adding CBMP to your enviroment]

    [*]Create a /libs folder in your modding enviroment. (Where the build.gradle file is)

    [*]Download the CBMP dev and src .jar from http://files.minecraftforge.net/ForgeMultipart/ and put them into the /libs folder.

    [*]Download the CodeChickenCore and NEI dev and src .jar form http://chickenbones.net/Pages/links.html and put them into the /libs folder.

    [*]Download the CodeChickenLib dev and src .jar from http://files.minecraftforge.net/CodeChickenLib/ and put them into the /libs folder.

    [*]You might want to copy the CodeChickenCore and NEI dev.jar into /eclipse/mods because they don't seem to be compiled in 1.7.10 (Check this yourself)

    [*]Finally link the dev.jar's as Librarys in your Enviroment and attach the sources. (You can simply Import both the same way in almost all IDE's and they will do the rest.)

    You should now be able to use CBMP and NEI. You can test CBMP by placing two torches in a single block.

     

     

     

    [spoiler=Creating your first part]

    It's finally time: After some installation horror we can begin with our first part. This part is just going to be a hitbox half the size of a block in the middle of it but we will later make a pipe out of this.

    You will also be able to place covers on its sides.

     

    For part registration and functionality you will need 3 Basic classes.

    The PartFactory which handles the registration itself, the PartItem you have in your inventory and the Actual part itself.

    Lets begin with the Part factory:    Right-click on the folder you want to have the class in and choose a new scala class / file (not a java class like you normally do) and call it whatever you want... I called mine PartFactory.

    
    
    package com.example.examplemod

    import codechicken.multipart.MultiPartRegistry
    import codechicken.multipart.MultiPartRegistry.IPartFactory

    //This is the actual registration class
    class PartFactory extends IPartFactory {

    //We call this method from our main mod class.
        def init() {
    //Parts are registered by using String maps. Each individual part / subtileEntity has a string like when you register a block.
            MultiPartRegistry.registerParts(this, Array[string](
                "examplePipe"
            ))
        }

    //This method is overridden from IPartFactory and returns a new Part.class matching the assigned string name.
        override def createPart(name: String, client: Boolean) = name match {
            case "examplePipe" => new ExamplePipe

            case _ => null
        }
    }

    //This is the static object used to call the Reg. class
    object PartRegistry extends PartFactory

     

    Next we want to take a look at the PartItem, which is basically a normal item implementing an Interface which makes it place a part in the world when you right-click with it.

    We, again, create a new scala file and call it Items or PartItems because we can use it later for multiple PartItem classes.

    
    
    package com.example.examplemod

    import codechicken.lib.vec.{Vector3, BlockCoord}
    import codechicken.multipart.{TItemMultiPart, MultiPartRegistry}
    import net.minecraft.entity.player.EntityPlayer
    import net.minecraft.item.{ItemStack, Item}
    import net.minecraft.world.World

    //We can add multiple classes like this one in this scala file.
    class ItemPartExamplePipe extends Item with TItemMultiPart {
        def newPart(item:ItemStack, player:EntityPlayer, world:World, pos:BlockCoord, side:Int, vhit:Vector3) = {
    //We create a new part.class and return it. Any modifications (set NBT data in the part by Item NBT) can be made here.
            val w = MultiPartRegistry.createPart("examplePipe", false).asInstanceOf[ExamplePipe]
            w
        }
    }

    Don't forget to register this item like any other item in your mod class.

     

    Last but not least we want to create the Part itself. It is a bit like the combination of a block and a TileEntity and has all methods for Rendering, Hit- / Boundingboxes, Updates, NBT data and many more.

    
    
    package com.example.examplemod

    import codechicken.lib.raytracer.IndexedCuboid6
    import codechicken.lib.vec.Cuboid6
    import codechicken.multipart.{TMultiPart, TNormalOcclusion}

    import scala.collection.JavaConverters._

    class ExamplePipe extends TMultiPart with TNormalOcclusion {
        //We have to return the pipes register name a last time.
        override def getType = "examplePipe"

        //Here we want to generate whatever boxes the Pipe is going to use later. For now it is only a center box but later we want to add sides for every possible connection.
        def generateBoxes = {
            import com.example.examplemod.PipeBoxes._
            var boxes = Seq(oBounds(0))
            boxes
        }

        //Here we return the collision boxes generated in generateBoxes(). We need to convert it to java because this method expects a java iterable not the scala one.
        def generateCollisionBoxes = {
            import mlb.technica.experience.PipeBoxes._
            var boxes = Seq(oBounds(0))
            boxes
        }

        //This method returns any occlusion boxes for the part to check if another part can be placed in the same space as this one.
        override def getOcclusionBoxes = {
            import mlb.technica.experience.PipeBoxes._
            Seq(oBounds(0)).asJava
        }

        //This method is used to return any sub parts that can be broken or clicked by the player to render their outlines. The connections the pipe makes to its sides are such subparts.
        override def getSubParts = {
            val b = generateBoxes
            var i = Seq[indexedCuboid6]()
            for (c <- b) i :+= new IndexedCuboid6(0, c)
            i.asJava
        }

    }

    //This Object stores all the possible boxes.
    object PipeBoxes {
        var oBounds = {
            val boxes = new Array[Cuboid6](1)
            val w = 2/8D
            boxes(0) = new Cuboid6(0.5-w, 0.5-w, 0.5-w, 0.5+w, 0.5+w, 0.5+w)
            boxes
        }
    }

     

    Finally we want to take a look at our examplemod class where everything should be registered.

    
    
    package com.example.examplemod;

    import net.minecraft.init.Blocks;
    import cpw.mods.fml.common.Mod;
    import cpw.mods.fml.common.Mod.EventHandler;
    import cpw.mods.fml.common.event.FMLInitializationEvent;

    @Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION)
    public class ExampleMod
    {
        public static final String MODID = "examplemod";
        public static final String VERSION = "1.0";
       
    public static Item item_ExamplePipe;

        @EventHandler
        public void init(FMLInitializationEvent event) {
    item_ExamplePipe = new ItemPartExamplePipe().setUnlocalizedName("ExamplePipe").setCreativeTab(someTab);
            PartRegistry.init();
            GameRegistry.registerItem(item_ExamplePipe, "ExamplePipe", MODID);
        }
    }

     

    You should now have a part that does nothing at all...

    Please make sure that there are not any missing Imports I might have forgotten.

     

     

     

    [spoiler=Using multiple Hit / Bounding-boxes]

    Having multiple Hit and Bounding-boxes is important for anything that goes beyond a simple cube. For our pipe we want to have a single bounding box in the middle of the part and six more for every side connection of the pipe. CBMP gives us the possibility to create each box that a part can have individually. (Bounding, Collision and Occlusion)

     

    The Bounding-boxes, which are called SubParts in CBMP, are used to generate boxes the player can interact with. They are also used to render the parts outline which can also be rendered individually.

     

    The Collision-boxes are used for collision testing with Entity's. It determines where you can stand on the part and where you can walk through.

     

    The Occlusion-boxes are used to test if your part collides with another so nothing else can be placed at the position of your part.

     

    The Hollow Cover is a good example for the use of different boxes since it uses a single Collision and Bounding-box but four occlusion boxes to create the hole in the middle.

     

    You can create multiple boxes by adding this simple code to your TMultiPart: (ExamplePipe)

    
    
    class ExamplePipe extends TMultiPart with TNormalOcclusion {
        //We have to return the pipes register name a last time.
        override def getType = "examplePipe"

        //Here we generate the collision boxes.
        def generateCollisionBoxes = {
            import mlb.technica.experience.PipeBoxes._
            var boxes = Seq(oBounds(6))
            for (s <- 0 until 6) boxes :+= oBounds(s)
            boxes
        }

    //Here we want to return the collision boxes generated in generateCollisionBoxes(). We have to convert it to java since it is expected.
    override def getCollisionBoxes = {
            generateCollisionBoxes.asJava
        }

        //This method returns any occlusion boxes for the part to check if another part can be placed in the same space as this one. We will just return the middle box for now as nothing can connect yet.
    //But we already added the structure for multiple boxes.
        override def getOcclusionBoxes = {
            import mlb.technica.experience.PipeBoxes._
            if (false)
            else Seq(oBounds(6)).asJava
        }

        //This method is used to return any sub parts that can be broken or clicked by the player to render their outlines. The connections the pipe makes to its sides are such subparts.
        override def getSubParts = {
            val b = generateCollisionBoxes
            var i = Seq[indexedCuboid6]()
            for (c <- b) i :+= new IndexedCuboid6(0, c)
            i.asJava
        }

    }

    //This Object stores all the possible boxes.
    object PipeBoxes {
        var oBounds = {
            val boxes = new Array[Cuboid6](7)
            val w = 2/8D
            boxes(6) = new Cuboid6(0.5-w, 0.5-w, 0.5-w, 0.5+w, 0.5+w, 0.5+w)
            for (s <- 0 until 6)
                boxes(s) = new Cuboid6(0.5-w, 0, 0.5-w, 0.5+w, 0.5-w, 0.5+w).apply(Rotation.sideRotations(s).at(Vector3.center))
            boxes
        }
    }

     

    This should provide a simple way to add more boxes to the part, by creating arrays of Cuboids. In the next tutorial we want to make them connect automatically.

     

     

     

     

    [spoiler=Rendering]

    CBMP has two main methods to render the part in the world but the bounding-box renderer can also be overridden.

    The part can be rendered using static and dynamic rendering. The static renderer uses the world renderer and only updates every time something in the world changes while the dynamic renderer updates every frame. It can be used for moving things as it also passes the partialTickTime.

     

    First of all we want to take a look at the static renderer.

    When using the static renderer the Tessellator is already running in quad mode and only draws when the world has rendered.

    It can be accessed by overriding the renderStatic method in the part. We also have to return if vertecies were added to the Tessellator.

    
    
    @SideOnly(Side.CLIENT)
        override def renderStatic(pos: Vector3, pass: Int) = {
    TextureUtils.bindAtlas(0) //First of all we want to bind the Block texture atlas just to be super safe...
    /*
    * All of your rendering code can go here but we have to register and use a texture first...
    */
    }

     

    Now that we have the renderer set up we can't simply bind a texture as it would also apply to every other block rendered by the world renderer. But we can register a texture to the main texture atlas where all the block textures are stored. To do so we can simply use the Item we created for the part.

     

    
    
    class ItemPartExamplePipe extends Item with TItemMultiPart {
    val sprites = new Array[iIcon](1) //For some reason it won't let me use a simple IIcon but we can use this for other parts...

        def newPart(item:ItemStack, player:EntityPlayer, world:World, pos:BlockCoord, side:Int, vhit:Vector3) = {
            val w = MultiPartRegistry.createPart("examplemod_examplepipe", false).asInstanceOf[examplepipe]
            w
        }

        @SideOnly(Side.CLIENT)
        override def registerIcons(reg:IIconRegister) {
            sprites(0) = reg.registerIcon("modid:parts/TextureName") //The texture is stored at assets/modid/textures/blocks/parts
        }

        /**
        * Returns 0 for /terrain.png, 1 for /gui/items.png
    * We use it to make the item not register the texture in the item texture atlas.
        */
        @SideOnly(Side.CLIENT)
        override def getSpriteNumber = 0
    }

     

    Now that we have the texture IIcon we can get the textures UV coordinates from it and use it for the tessellator.

    
    
    uMin = sprites(0).getMinU
    uMax = sprites(0).getMaxU
    vMin = sprites(0).getMinV
    vMax = sprites(0).getMaxV

     

    When we want to use the dynamic renderer we just have to start / draw / stop it. We can use the CCRenderState Library to do so. It also has methods for light adjusting and transparency, which might come in handy when you have light problems.

     

    
    
    override def renderDynamic(pos: Vector3, frame: Float, pass: Int): Unit = {
            TextureUtils.bindAtlas(0)
            CCRenderState.startDrawing()

    // Render stuff

    CCRenderState.draw()
    }

     

    Finally make sure that you only render solid stuff when the pass var is 1 and only render transparent stuff when the pass is 1. Never render stuff at both passes as it would just cause framerate lag. When you don't render something make sure to return false, otherwise the game will crash.

     

     

     

     


     

    More Tutorials will follow weekly...

    I apologize for any English mistakes. Please write a comment about them and any error you get in the code so I can improve the Tutorial.

     

    Overall Credits:

    • Chickenbones: For the great code.
    • MrTJP: For the great example that is ProjectRed.

     

    Any tutorials without credit are by me.

     

    Edited by Lex, CBMP has no realtion to Forge, so make that clean and changed FMP to CBMP.


  • Go to: Help / Install New Software
    [spoiler=Image]aUN8n.png 
  • Type the Update URL for the Scala plugin from http://scala-ide.org/download/current.html into the "Work with" field and click 'Add...'
    [spoiler=Image]aUORc.png 
  • Choose "Scala" as name and click 'OK'
  • Select all components and click 'Next'
    [spoiler=Image]aUOZX.png 
  • Click 'Next' again and accept all Licenses. Last but not least click 'Finish' to Install the plugins.
  • Finally right-click on your project and choose Configure / Add Scala Nature from the context menu.

PM's regarding modding questions should belong in the Modder Support sub-forum and won't be answered.

Link to comment
Share on other sites

There is no need to write your mod or multi-parts in scala.

You can also add CBMP to your project using the gradle.build file to have it automaticlaly download.

you should teach those routes.

I do Forge for free, however the servers to run it arn't free, so anything is appreciated.
Consider supporting the team on Patreon

Link to comment
Share on other sites

In the ExamplePipe class, you extend BasePipe. What class is that? I don't have that in my workspace? If i need to make it, what does it need to have?

Don't PM me with questions. They will be ignored! Make a thread on the appropriate board for support.

 

1.12 -> 1.13 primer by williewillus.

 

1.7.10 and older versions of Minecraft are no longer supported due to it's age! Update to the latest version for support.

 

http://www.howoldisminecraft1710.today/

Link to comment
Share on other sites

Oh sorry I forgot to delete that. It is actually for the piping tutorial and does all the connection work but I didn't got anything to render just yet.

You actually have to extend TMultiPart there.

PM's regarding modding questions should belong in the Modder Support sub-forum and won't be answered.

Link to comment
Share on other sites

  • 3 weeks later...

Glad to see some tutorials in Scala :). I found a bug and some cosmetic blemishes. Highlighted version on gist github - https://gist.github.com/mnn/57cc01980312289f39e9 (it's a working Scala script).

 

import scala.runtime.ScalaRunTime.stringOf
import scala.collection.JavaConverters._

// stuff for verifying results, skip to case defs (comment -- 1 --)
def doVerify[A](name: String, origRes: A, myRes: A) {
  val equals = origRes match {
    case a: Array[_] => a.sameElements(myRes.asInstanceOf[Array[_]])
    case _ => origRes.equals(myRes)
  }
  if (equals) println(s"Case '$name' fine: ${stringOf(myRes)}")
  else println(s"Case '$name' wrong:\n${stringOf(origRes)}\nvs.\n${stringOf(myRes)}")
}

def verify[A, R](name: String, orig: (A) => R, mine: (A) => R, testVal: A) {
  doVerify(name, orig(testVal), mine(testVal))
}

def verify[R](name: String, orig: () => R, mine: () => R) {
  doVerify(name, orig(), mine())
}

// -- 1 --
def case1orig(oBounds: Seq[int]) = {
  var boxes = Seq(oBounds(6))
  for (s <- 0 until 6) boxes :+= oBounds(s)
  boxes
}

def case1mine(oBounds: Seq[int]) = oBounds.last +: oBounds.init

verify("1", case1orig, case1mine, 0 to 6)
// -------

// -- 2 --
def case2orig() = {
  val w = "someExpression"
  w
}

def case2mine() = "someExpression"

verify("2", case2orig, case2mine)

// same with returning in generateBoxes, generateCollisionBoxes 
// -------

// -- 3 --
// Does getOcclusionBoxes even compile? There is no return value in "if" branch.
/*
    override def getOcclusionBoxes = {
        import mlb.technica.experience.PipeBoxes._
        if (true)
        else Seq(oBounds(6)).asJava
    }
*/

// Possibly what you meant:
def case3orig(oBounds: Seq[int]) = {
  if (true) Seq().asJava
  else Seq(oBounds(6)).asJava
}

def case3mine(oBounds: Seq[int]) = Seq().asJava

verify("3", case3orig, case3mine, 0 to 6)
// -------

// -- 4 --
// Some implementation of used classes in snippet
class Cuboid6(var a: Double, var b: Double, var c: Double, var d: Double, var e: Double, var f: Double) {
  def apply(r: Rotation): Cuboid6 = {
    a -= r.x
    b -= r.y
    c -= r.z
    d -= 7 * r.x
    e -= 9 * r.y
    f -= 13 * r.x
    this
  }

  override def equals(that: Any) =
    that match {
      case t: Cuboid6 => t.a == a && t.b == b && t.c == c && t.d == d && t.e == e && t.f == f
      case _ => false
    }

  override def toString() = s"Cuboid6($a, $b, $c, $d, $e, $f)"
}

class Rotation(val x: Double, val y: Double, val z: Double) {
  def at(v: Vector3): Rotation = new Rotation(x + v.x, y + 2 * v.y, z + 3 * v.z)
}

object Rotation {
  val sideRotations = (0 to 6).map(v => new Rotation(1.0 / (v + .1), 5.0 / (v + 2), 5 * (v - 3)))
}

class Vector3(val x: Double, val y: Double, val z: Double)

object Vector3 {
  lazy val center = new Vector3(11, 23, 27)
}

// PipeBoxes.oBounds
def case4orig() = {
  val boxes = new Array[Cuboid6](7)
  val w = 2 / 8D
  boxes(6) = new Cuboid6(0.5 - w, 0.5 - w, 0.5 - w, 0.5 + w, 0.5 + w, 0.5 + w)
  for (s <- 0 until 6)
    boxes(s) = new Cuboid6(0.5 - w, 0, 0.5 - w, 0.5 + w, 0.5 - w, 0.5 + w).apply(Rotation.sideRotations(s).at(Vector3.center))
  boxes
}

def case4mine() = {
  val w = 2 / 8D
  val (m, p) = (.5 - w, .5 + w)
  ((for (s <- 0 until 6) yield new Cuboid6(m, 0, m, p, m, p).apply(Rotation.sideRotations(s).at(Vector3.center)))
    :+ new Cuboid6(m, m, m, p, p, p)).toArray
}

verify("4", case4orig, case4mine)

mnn.getNativeLang() != English

If I helped you please click on the "thank you" button.

Link to comment
Share on other sites

OOps...

It was supposed to be an "if (false)" statement as we want to only return the middle box there because we still want to be able to place covers on the sides. If we were to return all of the boxes there we wouldn't be able to place those, they would fail the occlusion test.

We use the statement later to test for those boxes one at a time and test if a cover is placed or not. The result is then used to update the possible connections of the pipe.

I still need some time for the next tutorial since my code is still pretty messy and needs a lot of cleanup.

PM's regarding modding questions should belong in the Modder Support sub-forum and won't be answered.

Link to comment
Share on other sites

  • 3 weeks later...

I want to make a power cable with status, so when you right-click with a certain item it will show those stats.

So, is it like binding the part with the block? I'm trying to make something like the AE-2 cables (bounding boxes and parts). How did he do it? Because I can't see a way to make and binding a TileEntity for a part. Or can I do the tileEntity stuff in the part itself?

Link to comment
Share on other sites

I want to make a power cable with status, so when you right-click with a certain item it will show those stats.

So, is it like binding the part with the block? I'm trying to make something like the AE-2 cables (bounding boxes and parts). How did he do it? Because I can't see a way to make and binding a TileEntity for a part. Or can I do the tileEntity stuff in the part itself?

You need to do all the TileEntity stuff in the part itself, as you cannot bind a TileEntity to a TMultiPart

Don't PM me with questions. They will be ignored! Make a thread on the appropriate board for support.

 

1.12 -> 1.13 primer by williewillus.

 

1.7.10 and older versions of Minecraft are no longer supported due to it's age! Update to the latest version for support.

 

http://www.howoldisminecraft1710.today/

Link to comment
Share on other sites

A part is a bit like a mixture of the Part and the Block. It has all the interaction methods a block has. For instance onActivated() or onNeighbourChange().

You can use those to do stuff. Since parts already are TileEntitys (they are just specially coded ones) you can also interact with them. Have a look at the souces and look for the methods you could override.

PM's regarding modding questions should belong in the Modder Support sub-forum and won't be answered.

Link to comment
Share on other sites

  • 4 months later...

Yes I will.

I know that I saied to do it at the end of 2014 but I am currently in my last year of school and it takes about 90% of my free time.

Sorry -.-

No its fine. I completely understand. I am patiently waiting!wink.gif

I'm back from being gone for... I think its been about a year. I'm pretty sure nobody remembers me, but hello anybody who does!

Link to comment
Share on other sites

  • 3 months later...

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Announcements



×
×
  • Create New...

Important Information

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