├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── ansible ├── copy.yml └── deploy.yml ├── build.sbt ├── extra └── type.js ├── images └── kill.gif ├── project └── assembly.sbt ├── script ├── bootstrap.sh ├── build.sh ├── copy.sh ├── deploy.sh ├── install.sh └── run.sh └── src ├── main ├── resources │ └── plugin.yml └── scala │ └── pw │ └── ian │ └── sysadmincraft │ ├── SysAdmincraft.scala │ ├── commands │ ├── PgrepCommand.scala │ ├── PsCommand.scala │ └── TopCommand.scala │ ├── listeners │ ├── KillListener.scala │ └── MiscListener.scala │ ├── system │ ├── ProcessAdmin.scala │ └── SystemOverview.scala │ ├── tasks │ ├── PermaDayTask.scala │ └── PillarUpdateTask.scala │ └── world │ ├── PillarManager.scala │ ├── PillarWorldCreator.scala │ ├── ProcessPillar.scala │ └── WorldConstants.scala └── test └── scala └── pw └── ian └── sysadmincraft └── world └── PillarManagerSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/scala 2 | 3 | ### Scala ### 4 | *.class 5 | *.log 6 | 7 | # sbt specific 8 | .cache 9 | .history 10 | .lib/ 11 | dist/* 12 | target/ 13 | lib_managed/ 14 | src_managed/ 15 | project/boot/ 16 | project/plugins/project/ 17 | 18 | # Scala-IDE specific 19 | .scala_dependencies 20 | .worksheet 21 | .idea/ 22 | .swp 23 | 24 | server/ 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.7 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The ISC License (ISC) 2 | Copyright (c) 2015 Ian Macalinao 3 | 4 | Permission to use, copy, modify, and/or distribute this software for 5 | any purpose with or without fee is hereby granted, provided that the 6 | above copyright notice and this permission notice appear in all 7 | copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 10 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 11 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 12 | AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 13 | DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 14 | PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 15 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sysadmincraft 2 | 3 | [![Build Status](https://travis-ci.org/simplyianm/sysadmincraft.svg)](https://travis-ci.org/simplyianm/sysadmincraft) 4 | 5 | ![kill](/images/kill.gif) 6 | 7 | [View on YouTube][youtube] 8 | 9 | Admin your server in Minecraft! Inspired by the infamous [psDooM][psdoom] 10 | 11 | SysAdmincraft is a way to monitor your server through a fun interface. To kill a process, just kill a monster! Memory management is easy -- you can visually see what processes are taking the most memory on your server. No more `ps`, `top`, `uname -a`, etc! Just connect to a Minecraft server and start managing everything! 12 | 13 | The plugin also exposes a few commands, like: 14 | 15 | * `/pgrep ` -- Takes you to the column representing a process 16 | * `/ps` -- Lists all running processes belonging to your user 17 | * `/top` -- Takes you to the "top" of the map and shows `uname -a` and `uptime` output (and memory info if you're on a Mac) 18 | 19 | *Note: This is obviously a joke and is not meant to be used in production (or development, for that matter). A judge at the hackathon asked us how we were planning on monetizing it!* 20 | 21 | ## Running 22 | 23 | * Run `sbt assembly` to create the plugin. 24 | * `cd` to the `server/` directory. 25 | * Run `bootstrap.sh` to set up the Minecraft server. 26 | * Run `install.sh` to install the plugin. 27 | * Run `run.sh` to run the server. 28 | 29 | ## License 30 | 31 | ISC 32 | 33 | [psdoom]: http://psdoom.sourceforge.net/ 34 | [youtube]: https://youtu.be/rD9yJ9MHWzo 35 | -------------------------------------------------------------------------------- /ansible/copy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: ian@sysadmincraft-33yj7t51.cloudapp.net 3 | tasks: 4 | - name: Copy plugin to remote 5 | copy: src=../target/scala-2.11/SysAdmincraft.jar dest=./server/plugins/ 6 | -------------------------------------------------------------------------------- /ansible/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: ian@sysadmincraft-33yj7t51.cloudapp.net 3 | tasks: 4 | - name: Unarchive tarball onto remote 5 | unarchive: src=../target/package.tar.gz dest=. 6 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val root = (project in file(".")). 2 | settings( 3 | name := "sysadmincraft", 4 | version := "0.1.0", 5 | scalaVersion := "2.11.7" 6 | ) 7 | 8 | assemblyJarName in assembly := "SysAdmincraft.jar" 9 | 10 | resolvers += "Spigot" at "https://hub.spigotmc.org/nexus/content/repositories/snapshots/" 11 | 12 | libraryDependencies ++= Seq( 13 | "org.bukkit" % "bukkit" % "1.8.8-R0.1-SNAPSHOT" % "provided", 14 | "org.specs2" %% "specs2-core" % "3.6.5" % "test" 15 | ) 16 | 17 | scalacOptions in Test += "-Yrangepos" 18 | 19 | // META-INF discarding 20 | assemblyMergeStrategy in assembly := { 21 | case PathList("META-INF", xs @ _*) => MergeStrategy.discard 22 | case x => MergeStrategy.first 23 | } 24 | -------------------------------------------------------------------------------- /extra/type.js: -------------------------------------------------------------------------------- 1 | function winTypeRacer(){ 2 | var words = $('div.cw-QuotePanel-textToTypePanel').text().split(' '); 3 | typeWords(words, 150); 4 | } 5 | 6 | function typeWords(words, wpm) { 7 | $('.cw-TypedTextBox').val(words.shift()).keydown(function(e) { 8 | typeWords(words, wpm); 9 | }); 10 | } 11 | 12 | setTimeout(winTypeRacer, 3000) 13 | -------------------------------------------------------------------------------- /images/kill.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macalinao/sysadmincraft/3fbee945280358b267478c6548b81cb59f94f6f1/images/kill.gif -------------------------------------------------------------------------------- /project/assembly.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0") 2 | -------------------------------------------------------------------------------- /script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | DIR=`dirname $0` 4 | 5 | mkdir -p $DIR/../server 6 | curl http://tcpr.ca/files/spigot/spigot-1.8.8-R0.1-SNAPSHOT-latest.jar > $DIR/../server/spigot.jar 7 | -------------------------------------------------------------------------------- /script/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | DIR=`dirname $0` 4 | 5 | cd $DIR/.. 6 | sbt assembly 7 | -------------------------------------------------------------------------------- /script/copy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | DIR=`dirname $0` 4 | cd $DIR/.. 5 | 6 | ansible-playbook ansible/copy.yml 7 | -------------------------------------------------------------------------------- /script/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | DIR=`dirname $0` 4 | cd $DIR/.. 5 | 6 | script/install.sh 7 | 8 | tar czf target/package.tar.gz server/ 9 | ansible-playbook ansible/deploy.yml 10 | -------------------------------------------------------------------------------- /script/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | DIR=`dirname $0` 4 | cd $DIR/../ 5 | 6 | mkdir -p server/plugins/ 7 | cp target/scala-2.11/SysAdmincraft.jar server/plugins/ 8 | -------------------------------------------------------------------------------- /script/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | DIR=`dirname $0` 4 | 5 | cd $DIR/../server 6 | java -jar spigot.jar 7 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: SysAdmincraft 2 | description: Admin your server using Minecraft! 3 | version: 0.1.0 4 | main: pw.ian.sysadmincraft.SysAdmincraft 5 | commands: 6 | top: 7 | description: Top 8 | pgrep: 9 | description: pgrep 10 | ps: 11 | description: ps 12 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/SysAdmincraft.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft 2 | 3 | import pw.ian.sysadmincraft.commands.{PsCommand, TopCommand, PgrepCommand} 4 | import pw.ian.sysadmincraft.listeners.{KillListener, MiscListener} 5 | import pw.ian.sysadmincraft.tasks.{PermaDayTask, PillarUpdateTask} 6 | 7 | import scala.sys.process._ 8 | import org.bukkit.World 9 | import org.bukkit.plugin.java.JavaPlugin 10 | import pw.ian.sysadmincraft.world.{PillarManager, PillarWorldCreator} 11 | 12 | class SysAdmincraft extends JavaPlugin { 13 | 14 | var world: World = null 15 | 16 | var pillarManager: PillarManager = null 17 | 18 | override def onEnable() = { 19 | world = PillarWorldCreator.create("sysadmincraft") 20 | pillarManager = PillarManager(this, world) 21 | pillarManager.initPillars() 22 | getServer.getPluginManager.registerEvents(new KillListener(this), this) 23 | getServer.getPluginManager.registerEvents(new MiscListener(this), this) 24 | getCommand("pgrep").setExecutor(PgrepCommand(this)) 25 | getCommand("ps").setExecutor(PsCommand(this)) 26 | getCommand("top").setExecutor(TopCommand(this)) 27 | PermaDayTask(this).runTaskTimer(this, 100L, 100L) 28 | PillarUpdateTask(this).runTaskTimer(this, 100L, 100L) 29 | } 30 | 31 | override def onDisable() = { 32 | getLogger.info("Deleting world sysadmincraft...") 33 | 34 | // Delete the world 35 | "rm -rf sysadmincraft/" ! 36 | 37 | getLogger.info("World deleted.") 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/commands/PgrepCommand.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.commands 2 | 3 | import org.bukkit.ChatColor._ 4 | import org.bukkit.command.{Command, CommandSender, CommandExecutor} 5 | import org.bukkit.entity.Player 6 | import pw.ian.sysadmincraft.SysAdmincraft 7 | 8 | case class PgrepCommand(plugin: SysAdmincraft) extends CommandExecutor { 9 | 10 | override def onCommand(sender: CommandSender, command: Command, label: String, args: Array[String]): Boolean = { 11 | if (!sender.isInstanceOf[Player]) { 12 | sender.sendMessage(RED + "You can't use this command from the console.") 13 | return true 14 | } 15 | val player = sender.asInstanceOf[Player] 16 | 17 | if (args.length == 0) { 18 | player.sendMessage(RED + "Usage: /pgrep ") 19 | return true 20 | } 21 | 22 | val search = args.toList.mkString(" ") 23 | plugin.pillarManager.pillars.keys.find { key => 24 | key.toLowerCase.startsWith(search.toLowerCase) 25 | } match { 26 | case Some(key) => { 27 | val pillar = plugin.pillarManager.pillars.get(key).get 28 | player.teleport(pillar.location) 29 | player.sendMessage(YELLOW + s"Teleporting to process ${pillar.process.name}") 30 | } 31 | case None => { 32 | player.sendMessage(RED + s"Couldn't find process '$search'") 33 | } 34 | } 35 | true 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/commands/PsCommand.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.commands 2 | 3 | import org.bukkit.command.{Command, CommandExecutor, CommandSender} 4 | import org.bukkit.entity.Player 5 | import org.bukkit.{ChatColor, Location} 6 | import pw.ian.sysadmincraft.SysAdmincraft 7 | import pw.ian.sysadmincraft.system.SystemOverview 8 | import pw.ian.sysadmincraft.world.WorldConstants._ 9 | 10 | case class PsCommand(plugin: SysAdmincraft) extends CommandExecutor { 11 | 12 | override def onCommand(sender: CommandSender, command: Command, label: String, args: Array[String]): Boolean = { 13 | sender.sendMessage(ChatColor.YELLOW + "Running processes:") 14 | plugin.pillarManager.pillars.values.grouped(8).foreach { pillar => 15 | sender.sendMessage(pillar.map(_.process.name).mkString(", ")) 16 | } 17 | true 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/commands/TopCommand.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.commands 2 | 3 | import org.bukkit.{Location, ChatColor} 4 | import org.bukkit.command.{Command, CommandSender, CommandExecutor} 5 | import org.bukkit.entity.Player 6 | import pw.ian.sysadmincraft.SysAdmincraft 7 | import pw.ian.sysadmincraft.system.SystemOverview 8 | import pw.ian.sysadmincraft.world.WorldConstants._ 9 | 10 | case class TopCommand(plugin: SysAdmincraft) extends CommandExecutor { 11 | 12 | override def onCommand(sender: CommandSender, command: Command, label: String, args: Array[String]): Boolean = { 13 | if (!sender.isInstanceOf[Player]) { 14 | sender.sendMessage(ChatColor.RED + "You can't use this command from the console.") 15 | return true 16 | } 17 | val player = sender.asInstanceOf[Player] 18 | player.teleport(new Location(plugin.world, PILLAR_WIDTH + PATHWAY_WIDTH / 2, 19 | MAX_HEIGHT / 2, PILLAR_WIDTH + PATHWAY_WIDTH / 2, 90f, 0f)) 20 | 21 | player.sendMessage(ChatColor.GREEN + SystemOverview.uname()) 22 | player.sendMessage(ChatColor.BLUE + SystemOverview.uptime()) 23 | try { 24 | player.sendMessage(ChatColor.GREEN + "Total memory: " + ChatColor.YELLOW + SystemOverview.totalMem() + " MB") 25 | } catch { 26 | case _: Throwable => 27 | } 28 | 29 | true 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/listeners/KillListener.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.listeners 2 | 3 | import org.bukkit.event.{EventHandler, Listener} 4 | import org.bukkit.event.entity.{EntityDamageEvent, EntityDeathEvent} 5 | import pw.ian.sysadmincraft.SysAdmincraft 6 | 7 | case class KillListener(plugin: SysAdmincraft) extends Listener { 8 | 9 | @EventHandler 10 | def onEntityDeath(event: EntityDeathEvent): Unit = { 11 | plugin.pillarManager.handleDeath(event.getEntity.getCustomName) 12 | } 13 | 14 | @EventHandler 15 | def onEntityDamage(event: EntityDamageEvent): Unit = { 16 | if (event.getCause != EntityDamageEvent.DamageCause.ENTITY_ATTACK) { 17 | event.setCancelled(true) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/listeners/MiscListener.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.listeners 2 | 3 | import org.bukkit.entity.EntityType 4 | import org.bukkit.event.block.BlockBreakEvent 5 | import org.bukkit.event.entity.CreatureSpawnEvent 6 | import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason 7 | import org.bukkit.event.player.PlayerJoinEvent 8 | import org.bukkit.event.{EventHandler, Listener} 9 | import org.bukkit.inventory.ItemStack 10 | import org.bukkit.{Material, GameMode, Location} 11 | import pw.ian.sysadmincraft.SysAdmincraft 12 | import pw.ian.sysadmincraft.world.WorldConstants._ 13 | 14 | case class MiscListener(plugin: SysAdmincraft) extends Listener { 15 | 16 | @EventHandler 17 | def onPlayerJoin(event: PlayerJoinEvent): Unit = { 18 | event.getPlayer.setGameMode(GameMode.CREATIVE) 19 | event.getPlayer.teleport(new Location(plugin.world, 0, START_HEIGHT + 1, 0)) 20 | event.getPlayer.getInventory.addItem(new ItemStack(Material.IRON_SWORD, 1)) 21 | } 22 | 23 | @EventHandler 24 | def onBlockBreak(event: BlockBreakEvent): Unit = { 25 | event.setCancelled(true) 26 | } 27 | 28 | @EventHandler 29 | def onCreatureSpawn(event: CreatureSpawnEvent): Unit = { 30 | if (event.getSpawnReason == SpawnReason.NATURAL || event.getEntityType == EntityType.SLIME) { 31 | event.setCancelled(true) 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/system/ProcessAdmin.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.system 2 | 3 | import pw.ian.sysadmincraft.world.WorldConstants 4 | 5 | import sys.process._ 6 | 7 | case class SysProcess(name: String, ids: Set[Int], realMemory: Long, virtualMemory: Long, 8 | cpuPct: Double, memPct: Double, stat: String, time: String) { 9 | 10 | def totalMemory = realMemory + virtualMemory 11 | 12 | def memAmt: Double = Math.min(totalMemory.toDouble / WorldConstants.MAX_MEMORY, 1) 13 | 14 | def kill() = { 15 | ids.foreach(x => s"kill -9 $x" !) 16 | } 17 | 18 | } 19 | 20 | object ProcessAdmin { 21 | 22 | def processes(): List[SysProcess] = { 23 | findUserProcesses().map { case(key, value) => 24 | SysProcess(key, value._1, value._2, value._3, value._4, value._5, value._6, value._7) 25 | }.toList 26 | } 27 | 28 | private def findUserProcesses(): Map[String, (Set[Int], Long, Long, Double, Double, String, String)] = { 29 | rawProcessOutput().split('\n').tail.map(_.split(' ').filter(!_.isEmpty)) 30 | .filter(!_(0).startsWith("root")) 31 | .filter(!_(0).startsWith("_")) 32 | .map(_.tail) 33 | .map(x => x.slice(7, x.length).mkString(" ") -> ( 34 | x(0).toInt, // pid 35 | x(1).toLong, // rss 36 | x(2).toLong / 1024, // vsz 37 | x(3).toDouble, // cpu 38 | x(4).toDouble, // mem 39 | x(5), // stat 40 | x(6) // time 41 | )) 42 | .groupBy(_._1) // Group by process name 43 | .mapValues(_.map(_._2)) 44 | .mapValues { 45 | _.foldLeft((Set[Int](), 0L, 0L, 0d, 0d, "", "")) { (acc, v) => 46 | (acc._1 + v._1, acc._2 + v._2, acc._3 + v._3, 47 | acc._4 + v._4, acc._5 + v._5, v._6, v._7) 48 | } 49 | } 50 | } 51 | 52 | private def rawProcessOutput(): String = { 53 | "ps axco user,pid,rss,vsz,%cpu,%mem,stat,time,command" !! 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/system/SystemOverview.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.system 2 | 3 | import sys.process._ 4 | 5 | object SystemOverview { 6 | 7 | def uname(): String = { 8 | "uname -a" !! 9 | } 10 | 11 | def uptime(): String = { 12 | "uptime" !! 13 | } 14 | 15 | def totalMem(): Int = { 16 | (("ps caxm -orss" !!).split("\n").tail.map(_.trim.toLong).sum / 1024).toInt 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/tasks/PermaDayTask.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.tasks 2 | 3 | import org.bukkit.scheduler.BukkitRunnable 4 | import pw.ian.sysadmincraft.SysAdmincraft 5 | 6 | case class PermaDayTask(plugin: SysAdmincraft) extends BukkitRunnable { 7 | 8 | override def run(): Unit = { 9 | plugin.world.setTime(6000L) 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/tasks/PillarUpdateTask.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.tasks 2 | 3 | import org.bukkit.scheduler.BukkitRunnable 4 | import pw.ian.sysadmincraft.SysAdmincraft 5 | import pw.ian.sysadmincraft.system.ProcessAdmin 6 | 7 | case class PillarUpdateTask(plugin: SysAdmincraft) extends BukkitRunnable { 8 | 9 | override def run() = { 10 | plugin.pillarManager.refresh(ProcessAdmin.processes()) 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/world/PillarManager.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.world 2 | 3 | 4 | import java.util.UUID 5 | 6 | import org.bukkit.World 7 | import pw.ian.sysadmincraft.system.{SysProcess, ProcessAdmin} 8 | import pw.ian.sysadmincraft.SysAdmincraft 9 | import pw.ian.sysadmincraft.world.WorldConstants._ 10 | 11 | case class PillarManager(plugin: SysAdmincraft, world: World) { 12 | 13 | // process.name -> pillar 14 | var pillars = Map[String, ProcessPillar]() 15 | 16 | var taken = Set[Int]() 17 | 18 | def initPillars(): List[ProcessPillar] = { 19 | ProcessAdmin.processes().sortBy(-_.totalMemory).zipWithIndex.map { case (process, index) => 20 | buildPillar(index, process) 21 | } 22 | } 23 | 24 | def refresh(processes: Iterable[SysProcess]) = { 25 | processes.foreach { process => 26 | pillars.get(process.name) match { 27 | case Some(pillar) => pillar.update(process) 28 | case None => buildPillar(nextFreeIndex, process) 29 | } 30 | } 31 | // Destroy pillars that are missing 32 | (pillars.keySet &~ processes.map(_.name).toSet).foreach { name => 33 | removePillar(pillars.get(name).get) 34 | } 35 | } 36 | 37 | def buildPillar(index: Int, process: SysProcess) = { 38 | val pillar = ProcessPillar(index, blockFromIndex(index), process) 39 | pillars += process.name -> pillar 40 | taken += index 41 | pillar 42 | } 43 | 44 | def handleDeath(name: String): Unit = { 45 | pillars.get(name) match { 46 | case Some(pillar) => destroyPillar(pillar) 47 | case None => 48 | } 49 | } 50 | 51 | def destroyPillar(pillar: ProcessPillar) = { 52 | removePillar(pillar) 53 | pillar.kill() 54 | plugin.getServer.broadcastMessage(s"Process ${pillar.process.name} has been killed.") 55 | } 56 | 57 | def removePillar(pillar: ProcessPillar) = { 58 | taken -= pillar.index 59 | pillars -= pillar.process.name 60 | pillar.teardown() 61 | } 62 | 63 | private def nextFreeIndex: Int = Stream.from(0).find(!taken.contains(_)).get 64 | 65 | private def blockFromIndex(index: Int) = { 66 | val i = PillarManagerUtils.spiralIndex(index) 67 | world.getBlockAt(i._1 * PILLAR_DISTANCE + (PILLAR_DISTANCE / 2), 68 | START_HEIGHT, i._2 * PILLAR_DISTANCE + (PILLAR_DISTANCE / 2)) 69 | } 70 | } 71 | 72 | object PillarManagerUtils { 73 | 74 | def spiralIndex(n: Int): (Int, Int) = { 75 | // given n an index in the squared spiral 76 | // p the sum of point in inner square 77 | // a the position on the current square 78 | // n = p + a 79 | val r = (Math.floor((Math.sqrt(n + 1) - 1) / 2) + 1).toInt 80 | 81 | // compute radius : inverse arithmetic sum of 8+16+24+...= 82 | val p = (8 * r * (r - 1)) / 2 83 | // compute total point on radius -1 : arithmetic sum of 8+16+24+... 84 | 85 | val en = r * 2 86 | // points by face 87 | 88 | val a = (1 + n - p) % (r * 8) 89 | // compute de position and shift it so the first is (-r,-r) but (-r+1,-r) 90 | // so square can connect 91 | 92 | val m = Math.floor(a / (r * 2)).toInt 93 | 94 | ( 95 | m match { 96 | case 0 => a - r 97 | case 1 => r 98 | case 2 => r - (a % en) 99 | case 3 => -r 100 | }, 101 | m match { 102 | case 0 => -r 103 | case 1 => (a % en) - r 104 | case 2 => r 105 | case 3 => r - (a % en) 106 | } 107 | ) 108 | } 109 | 110 | 111 | } -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/world/PillarWorldCreator.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.world 2 | 3 | import java.util.Random 4 | 5 | import org.bukkit.{WorldCreator, Material, World} 6 | import org.bukkit.generator.ChunkGenerator 7 | import org.bukkit.generator.ChunkGenerator.BiomeGrid 8 | 9 | class PillarWorldGenerator extends ChunkGenerator { 10 | 11 | override def generateBlockSections(world: World, random: Random, x: Int, z: Int, biomeGrid: BiomeGrid): Array[Array[Byte]] = { 12 | val ret = Array.ofDim[Byte](16, 4096) 13 | (0 to 4095).foreach(ret(0)(_) = Material.GRASS.getId.asInstanceOf[Byte]) 14 | ret 15 | } 16 | 17 | } 18 | 19 | object PillarWorldCreator { 20 | 21 | def create(name: String): World = { 22 | new WorldCreator(name).generator(new PillarWorldGenerator).createWorld() 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/world/ProcessPillar.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.world 2 | 3 | import org.bukkit.Material 4 | import org.bukkit.block.{Sign, Block} 5 | import org.bukkit.entity.{EntityType, LivingEntity} 6 | import pw.ian.sysadmincraft.system.SysProcess 7 | import pw.ian.sysadmincraft.world.WorldConstants._ 8 | 9 | case class ProcessPillar(index: Int, base: Block, var process: SysProcess) { 10 | 11 | var height = 0 12 | update(process) 13 | val mob = setupMob() 14 | 15 | def update(process: SysProcess) = { 16 | assert(this.process.name == process.name) 17 | val newHeight = memToHeight(process.totalMemory) 18 | if (newHeight > height) { 19 | construct(height + 1, newHeight, Material.GOLD_BLOCK) 20 | } else { 21 | destruct(newHeight + 1, height) 22 | } 23 | destroyCpu() 24 | buildCpu(cpuToHeight(process.cpuPct)) 25 | setupFence() 26 | updateStats() 27 | this.process = process 28 | this.height = newHeight 29 | } 30 | 31 | def teardown() = { 32 | destruct(0, height) 33 | destroyCpu() 34 | clearBase() 35 | base.getRelative(0, 2, -1).setType(Material.AIR) 36 | } 37 | 38 | def kill() = { 39 | mob.remove() 40 | process.kill() 41 | } 42 | 43 | def location = { 44 | val location = base.getLocation.add(PILLAR_WIDTH / 2, 0, -2) 45 | location.setPitch(0f) 46 | location.setYaw(0f) 47 | location 48 | } 49 | 50 | private def memToHeight(memoryUsage: Long) = { 51 | Math.min(WorldConstants.MAX_HEIGHT, 52 | ((memoryUsage.toDouble / MAX_MEMORY) * MAX_HEIGHT).toInt) 53 | } 54 | 55 | private def cpuToHeight(cpuPct: Double): Int = { 56 | (WorldConstants.MAX_HEIGHT * cpuPct / 100d).toInt 57 | } 58 | 59 | private def updateStats(): Unit = { 60 | val leftBlock = base.getRelative(PILLAR_WIDTH - 1, 2, -1) 61 | if (leftBlock.getType != Material.WALL_SIGN) { 62 | leftBlock.setType(Material.WALL_SIGN) 63 | } 64 | val leftSign = leftBlock.getState.asInstanceOf[Sign] 65 | leftSign.setLine(0, process.name) 66 | leftSign.setLine(1, "Real: " + process.realMemory) 67 | leftSign.setLine(2, "Virtual: " + process.virtualMemory) 68 | leftSign.setLine(3, "Count: " + process.ids.size) 69 | leftSign.update(true) 70 | val rightBlock = base.getRelative(0, 2, -1) 71 | if (rightBlock.getType != Material.WALL_SIGN) { 72 | rightBlock.setType(Material.WALL_SIGN) 73 | } 74 | val rightSign = rightBlock.getState.asInstanceOf[Sign] 75 | rightSign.setLine(0, f"CPU %%: ${process.cpuPct}%.2f") 76 | rightSign.setLine(1, f"MEM %%: ${process.memPct}%.2f") 77 | rightSign.setLine(2, s"Stat: ${process.stat}") 78 | rightSign.setLine(3, s"Time: ${process.time}") 79 | rightSign.update(true) 80 | } 81 | 82 | /** 83 | * Spawns a mob that represents this process 84 | * 85 | * Should be: 86 | * - have name of process as name 87 | * - be a different mob depending on memory size 88 | * 89 | * @return the entity 90 | */ 91 | private def setupMob() = { 92 | val entity = base.getWorld.spawnEntity(base.getLocation.add(PILLAR_WIDTH / 2, -MOB_HOUSE_DEPTH, PILLAR_WIDTH / 2), process.memAmt match { 93 | case x if x <= 0.2 => EntityType.CHICKEN 94 | case x if x <= 0.4 => EntityType.PIG 95 | case x if x <= 0.6 => EntityType.ZOMBIE 96 | case x if x <= 0.8 => EntityType.SPIDER 97 | case _ => EntityType.BLAZE 98 | }).asInstanceOf[LivingEntity] 99 | entity.setCustomName(process.name) 100 | entity.setCustomNameVisible(true) 101 | entity 102 | } 103 | 104 | /** 105 | * The fence replaces the base of the tower with air and a fence 106 | */ 107 | private def setupFence(): Unit = { 108 | //set all of the blocks in bottom 4 rows of the pillar to glass 109 | for { 110 | x <- 0 until PILLAR_WIDTH 111 | y <- 0 until MOB_HOUSE_HEIGHT 112 | z <- 0 until PILLAR_WIDTH 113 | } base.getRelative(x, y, z).setType(Material.GLASS) 114 | 115 | // Bottom hole 116 | for { 117 | x <- 0 until PILLAR_WIDTH 118 | y <- -MOB_HOUSE_DEPTH until 0 // dig underground 119 | z <- 0 until PILLAR_WIDTH 120 | } base.getRelative(x, y, z).setType(Material.AIR) 121 | 122 | // Top hole 123 | for { 124 | x <- 1 until PILLAR_WIDTH - 1 125 | y <- 0 until MOB_HOUSE_HEIGHT - 1 126 | z <- 0 until PILLAR_WIDTH - 1 127 | } base.getRelative(x, y, z).setType(Material.AIR) 128 | } 129 | 130 | private def clearBase(): Unit = { 131 | 132 | //set all of the blocks in bottom 4 rows of the pillar to air 133 | for { 134 | x <- 0 until PILLAR_WIDTH 135 | y <- 0 until MOB_HOUSE_HEIGHT 136 | z <- 0 until PILLAR_WIDTH 137 | } base.getRelative(x, y, z).setType(Material.AIR) 138 | 139 | // Bottom hole 140 | for { 141 | x <- 0 until PILLAR_WIDTH 142 | y <- -MOB_HOUSE_DEPTH until 0 // dig underground 143 | z <- 0 until PILLAR_WIDTH 144 | } base.getRelative(x, y, z).setType(Material.GRASS) 145 | 146 | } 147 | 148 | private def construct(startHeight: Int, endHeight: Int, blockType: Material): Unit = 149 | blocks(startHeight, endHeight).foreach(_.setType(blockType)) 150 | 151 | private def destruct(startHeight: Int, endHeight: Int): Unit = 152 | blocks(startHeight, endHeight).foreach(_.setType(Material.AIR)) 153 | 154 | private def destroyCpu() = { 155 | for { 156 | x <- List(0, 3) 157 | y <- 0 until 256 // clear all world 158 | z <- List(0, 3) 159 | } base.getRelative(x, y, z).setType(Material.AIR) 160 | } 161 | 162 | private def buildCpu(height: Int) = { 163 | for { 164 | x <- List(0, 3) 165 | y <- 0 until height 166 | z <- List(0, 3) 167 | } base.getRelative(x, y, z).setType(Material.DIAMOND_BLOCK) 168 | } 169 | 170 | private def blocks(startHeight: Int, endHeight: Int): IndexedSeq[Block] = 171 | for { 172 | level <- startHeight to endHeight 173 | x <- base.getX + PILLAR_PADDING until base.getX + PILLAR_WIDTH - PILLAR_PADDING 174 | z <- base.getZ + PILLAR_PADDING until base.getZ + PILLAR_WIDTH - PILLAR_PADDING 175 | } yield base.getWorld.getBlockAt(x, level + START_HEIGHT + MOB_HOUSE_HEIGHT - 1, z) 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/main/scala/pw/ian/sysadmincraft/world/WorldConstants.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.world 2 | 3 | object WorldConstants { 4 | 5 | val START_HEIGHT = 16 6 | 7 | val MAX_HEIGHT = 96 - START_HEIGHT 8 | 9 | val MAX_MEMORY = 2L * 1024 * 1024 // 2GB 10 | 11 | val PILLAR_WIDTH = 4 12 | 13 | val PILLAR_PADDING = 1 14 | 15 | val PATHWAY_WIDTH = 4 16 | 17 | val PILLAR_DISTANCE = PILLAR_WIDTH + PATHWAY_WIDTH 18 | 19 | val MOB_HOUSE_HEIGHT = 4 20 | 21 | val MOB_HOUSE_DEPTH = 2 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/scala/pw/ian/sysadmincraft/world/PillarManagerSpec.scala: -------------------------------------------------------------------------------- 1 | package pw.ian.sysadmincraft.world 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | class PillarManagerSpec extends Specification { 6 | 7 | "spiralIndex" >> { 8 | 9 | "should be unique" >> { 10 | 11 | val list = (0 to 10000).map(PillarManagerUtils.spiralIndex) 12 | list.toSet.size must_== list.size 13 | 14 | } 15 | 16 | } 17 | 18 | } --------------------------------------------------------------------------------