Something Fun

I casually play minecraft from time to time and run a small server for group play. Things are kept mostly vanilla, but code and hacking excites me as much as (or more than) mining and crafting. I wanted a way to do something simple – say boom in chat, and wherever I am in world, a small fireworks jubilee sets off. Written in nodejs/javascript for convenience.

OK, for starters, this post isn't a walk-through. It won't explain things like directory references, but those should be self-explanatory. The goal of this post is to show the code that makes it go boom. First, to read the chat, just tail the server log file. I'm using a helper library that handles things like reconnecting to the file when the log rotates.

const { Tail } = require(`${__dirname}/Tail.js`)
let tail = new Tail(`${__dirname}/../logs/latest.log`)
tail.on('line', line => {
  let matched = line.match(/^\[\d\d:\d\d:\d\d\] \[Server thread\/INFO\]: <(.+?)> (.+)$/)
  if(matched){
    let action = matched[2].toLowerCase()
    debug(`${matched[1]} said ${matched[2]}`)
    if(action == "boom") boom(matched[1])
  }
})
tail.on('error', err =>{ debug(`ERROR: ${err}`) })

The second library for interacting with the server is rcon-client, which allows the script to send commands (and queries) to the server. This also means the server needs RCON enabled.

const { Rcon } = require(`${__dirname}/rcon-client/lib/`)
const rcon = new Rcon({host:'localhost', port:25575, password:'apass'})
rcon.on('authenticated', ()=>{ say(`Hey everybody! HAL reconnected.`) })
rcon.on('end', ()=>{ debug(`RCON ended`) })
rcon.on("error", err=>{ debug(`RCON Error ${err}`) })
async function init(){
  debug(`Going to initalize the RCON`)
  await rcon.connect()
}
init().catch(debug)

So those are the two pieces that handle communication with the server. Now it's time for the fun stuff – making the fireworks go boom. First things first, the script needs to find the player position when the player says boom. The script uses data get entity <player> Pos.

const { Firework } = require('./boom')
function rI(max=10,abs=false){ //just a random int helper func.
  let i = Math.floor(Math.random() * Math.floor(max))
  let a = Math.round(Math.random())
  if(a||abs) return i
  return -i
}

async function boom(player){
  let pos = await rcon.send(`data get entity ${player} Pos`)
  let coords = pos.match(/\[(.+?), (.+?), (.+?)\]/)
  if(coords){
    coords.forEach((m,i)=>{ coords[i] = parseInt(m) })
    let count = rI(60,true)
    debug(`Launching ${count} fireworks!`)
    for(var x=1; x<count; x++){
      let fwcmd = `summon firework_rocket ${coords[1]+rI()} ${coords[2]} ${coords[3]+rI()} ${Firework.make()}`
      //debug(fwcmd)
      setTimeout(()=>{ rcon.send(fwcmd) }, rI(3000))
    }
  }
}

The final piece is making the fireworks. For this, I checked out digminecraft's fireworks generator. The script uses the /summon variant to summon fireworks near the player. Looking at the boom function in the previous block, fireworks are summoned on a random interval in a randomized 10-block grid around the player. Because the syntax of the firework command is a bit messy, I wrote a small function to contain the mess. Elements of the fireworks are randomized.

function firework(){
  function randomBinary(){ return Math.round(Math.random()) }
  function randomInt(max=1,abs=true){ //TODO: might exclude max.
    let i = Math.floor(Math.random() * Math.floor(max))
    if(abs || randomBinary()) return i
    return -i
  }
  let LIFE = [ 15,20,25,30,35,40,45,50,55,60 ]
  let COLORS = [
    1973019, //Black
    2651799, //Cyan
    6719955, //Light Blue
    11743532,//Red
    11250603,//Light Gray
    12801229,//Magenta
    3887386, //Green
    4408131, //Gray
    15435844,//Orange
    5320730, //Brown
    14188952,//Pink
    15790320,//White
    2437522, //Blue
    4312372, //Lime
    8073150, //Purple
    14602026,//Yellow
  ]
  let getColors = function(){
    let colors = []
    for(x=0;x<=randomInt(4);x++){
      let color = randomInt(COLORS.length-1)
      colors.push(COLORS[color])
    }
    return colors
  }
  let getExplosion = function(){
    let explosion = {
      Type: randomInt(4).toString(), //['small', 'big', 'star', 'creeper', 'burst']
      Flicker: randomBinary().toString(),
      Trail: randomBinary().toString(),
      Colors: getColors()
    }
    if(randomBinary()) explosion.FadeColors = getColors()
    return explosion
  }
  this.make = function(){
    let rocket = {
      LifeTime: LIFE[randomInt(LIFE.length)],
      FireworksItem: {
        id: "firework_rocket",
        Count: 1,//randomInt(4)+1,
        tag: { Fireworks: {
          Flight: (randomInt(3)+1).toString(),
          Explosions: [ getExplosion() ]
        } }
      }
    }
    //Turn javascript object into minecraft command syntax:
    return JSON.stringify(rocket).replace(/"/g,'').replace(/Colors:\[/g,'Colors:[I;')
  }
}
exports.Firework = new firework()