Minecraft – ntzyz's blog https://archive.ntzyz.io Mon, 18 Sep 2017 12:18:12 +0000 zh-CN hourly 1 https://wordpress.org/?v=5.8 [水] Minecraft 服务器周边功能小记 https://archive.ntzyz.io/2016/07/21/things-around-the-minecraft-server/ https://archive.ntzyz.io/2016/07/21/things-around-the-minecraft-server/#comments Thu, 21 Jul 2016 03:09:58 +0000 https://blog.dimension.moe/?p=671 继续阅读[水] Minecraft 服务器周边功能小记]]> p.indent { text-indent: 2em; }

按照以往运行 Minecraft 服务器的情况,玩家们常常需要一些游戏以外的信息:

  • 服务器负载情况
  • 服务器是否还活着
  • 是死宅要上传皮肤

那么,作为一个不怎么管服务器里发生了什么的管理,我就主动把这些功能实现了一下。其他的游戏内管理就交给 @kasora 了。

服务器负载

我们目前使用的是腾讯机房的双核 4G 的云服务器,讲道理这种阵容并不适合跑 MC 这种单线程大作。与此同时这次服务器应要求添加了村庄Mod(Millenaire),而 Millenaire 的作者自述此 Mod 和 Bukkit 存在兼容性问题,直接导致我们不再能使用 Bukkit 或者是基于 Bukkit 的服务端(例如 Cauldron),进而失去了 Bukkit API 和民间带来的一些优化。于是,在任意时刻知道服务器负荷以及一段时间内的负荷就显得比较有意义了。

至于如何去实现,就比较简单了。由于备案之类的种种问题,我们运行着 Minecraft Server 的服务器原则上不可以运行任何 HTTP Server 服务,所以我的思路是本机采集数据,然后通过某些办法发送到另一个已经备案了的服务器。

收集数据

在 Linux 设备上,我们有很多途径可以知道设备的负载和内存开销。系统负载的话,比如 `top`、`uptime` 命令,或者是 `/proc/loadavg` 这类文件,同样系统时间和内存占用也不难获得。于是我们可以准备这么一个 shell 脚本:

#!/bin/bash

TIMESTAMP=`date +%s`
LOAD=`cat /proc/loadavg | awk '{print $1}'`
MEM=`free -m | grep 'Mem' | awk '{print $3}'`

URL="https://status.mc.ntzyz.cn/set.php?timestamp=$TIMESTAMP&mem=$MEM&load=$LOAD"

curl $URL > /dev/null

执行一次,就能得到当前时间戳,1 分钟内负载和内存使用,并把这些数据作为请求发送到 status.mc.ntzyz.cn 所在的服务器了。这个脚本需要定时执行,使用 `crontab` 就可以完成。执行 `crontab -e`,并在文末添上:

* * * * * /bin/bash /home/ntzyz/send.sh

就可以实现每分钟一次请求了。

记录数据

服务器的各种数据发送到 status.mc.ntzyz.cn 之后,就需要将这些数据保存下来,准备随时被需要数据的情况下使用。是时候让全世界最好的语言上场了(雾

status = "ERROR";
    $res->message = "You shall not access.";
    die(json_encode($res));
  }

  $conn = mysql_connect("localhost", "_(:3」∠)_", "(╯-_-)╯╧╧");
  if (!$conn) {
    $res->status = "ERROR";
    $res->message = "Could not connect: " . mysql_error();
    die(json_encode($res));
  }

  $mem_used = $_GET["mem"];
  $cpu_load = $_GET["load"];
  $timestamp = time();
  if (!isset($mem_used) || !isset($cpu_load)) {
    $res->status = "ERROR";
    $res->message = "required field(s) is empty.";
    die(json_encode($res));
  }

  mysql_select_db("mc_stat", $conn);
  $sql = "insert into mc_stat(timestamp, cpu_load, mem_used) values('$timestamp', '$cpu_load', '$mem_used')";

  if (!mysql_query($sql,$conn)) {
    $res->status = "ERROR";
    $res->message = "SQL Error: " . mysql_error();
    die(json_encode($res));
  }
  else {
    $res->status = "success";
    echo(json_encode($res));
  }
?>

这样你就可以将使用 cURL 发来的数据保存到数据库了。然后就是再写一段脚本来读取这些数据,准备让其他人读取。

status = "ERROR";
    $res->message = "Could not connect: " . mysql_error();
    die(json_encode($res));
  }

  mysql_select_db("mc_stat", $conn);

  $res = mysql_query("select * from mc_stat order by timestamp desc limit $limit                                                                              ");
  $i = 0;

  $resp->status = "success";
  $resp->data = array();

  while ($row = mysql_fetch_array($res)) {
    $resp->data[$i]->timestamp = $row["timestamp"];
    $resp->data[$i]->cpu_load = $row["cpu_load"];
    $resp->data[$i]->mem_used = $row["mem_used"];
    $i = $i + 1;
  }

  die(json_encode($resp));
?>

数据的保存和读取就都完成了。

显示数据

世界上没什么比图形好的数据可视化方案了。我选择使用 highcharts 这个图表框架。由于只需要简单的显示两张折线图(负载和内存),我就直接对着 Demo 改了改,也算是 Make data come alive 了。

两张表的结构基本一致,所以只要准备一份模板,然后两次填入数据并使用框架就行了。首先是模板:

var template = {
  title: {
    text: 'Monthly Average Temperature',
    x: -20 //center
  },
  xAxis: {
    categories: null
  },
  yAxis: {
    plotLines: [{
      value: 0,
      width: 1,
      color: '#808080'
    }]
  },
  tooltip: {
    valueSuffix: ''
  },
  legend: {
    layout: 'vertical',
    align: 'right',
    verticalAlign: 'middle',
    borderWidth: 0
  },
  series: [{
    name: 'Tokyo',
    data: []
  }]
};

嗯,稍微了解过 Highcharts 的人都能看出,这基本就是第一个 Demo —— Basic Lines 的内容,只是少了一些个数据。然后就是使用 Ajax,将保存在 status.mc.ntzyz.cn 上的数据获得到浏览器,填入 template 并显示。

  $.ajax({
    url: "https://status.mc.ntzyz.cn/get.php?limit=" + limit,
    type: "GET",
    beforeSend: function() {
      $('#loadchart').html('获取数据中');
      $('#memchart').html('获取数据中');
    }
  }).done(function(data) {
    var res = JSON.parse(data);
    template.xAxis.categories = Array(limit).fill(0).map(function (it, off) {return limit - off;});
    template.title.text = 'Load Average';
    template.series[0].name = 'Load';
    template.series[0].data = Array(limit).fill(0).map(function (it, off) {return res.data[off].cpu_load * 1;}).reverse();
    $('#loadchart').highcharts(template);
    template.title.text = 'Memory Used';
    template.series[0].name = 'MB';
    template.series[0].data = Array(limit).fill(0).map(function (it, off) {return res.data[off].mem_used * 1;}).reverse();
    $('#memchart').highcharts(template);
  });

这样,就完成了所有的数据采集、记录和显示工作了。在那段 PHP 脚本中可以看出,只要修改 limit 的值,就能获得不同数量的数据,因此短时间的数据显示和长时间的数据显示就都可以完成了。

皮肤配置、上传与预览

去年暑假剁了正版 Minecraft 之后才知道这个游戏是有皮肤这个设定的,然而在 Online Mode 设置为 Off 的服务器上,客户端并不会去从 Mojang 的服务器上获得其他玩家的皮肤数据,这样别人就看不到我的皮肤了(雾

皮肤配置

由于没有使用正版验证的玩家是不能通过官方途径更换自己的皮肤,我们需要使用额外的 Mod 来实现皮肤的自定义。这里我们使用了 UniSkinMode配置的方法也不算麻烦。

皮肤上传

按照 UniSkinMod 的 API,我们需要准备每个玩家的 JSON 信息,同时准备一个 textures 目录来存储 PNG 格式的皮肤。上传皮肤只需要修改对应的 JSON 文件,同时添加/更新 textures 目录下对应的文件即可。综上,这些工作只需要一段 PHP 就可以完成。

皮肤预览

做这个纯粹是我比较想折腾折腾,毕竟 WebGL 出来这么多年了,同时也有 Three.js 这种很好用的 JavaScript 3D 库。这里的工作相当繁杂(然而逻辑很简单,都特么 UV 贴图数据要手撕)。想了解的可以直接去看下源码

]]>
https://archive.ntzyz.io/2016/07/21/things-around-the-minecraft-server/feed/ 4
[Minecraft] 服务器大地图~ https://archive.ntzyz.io/2015/10/23/big-map-of-the-minecraft-server/ https://archive.ntzyz.io/2015/10/23/big-map-of-the-minecraft-server/#respond Fri, 23 Oct 2015 04:33:41 +0000 https://blog.dimension.moe/?p=229 继续阅读[Minecraft] 服务器大地图~]]> Minecraft服务器开了好久了,这次Kasora同学拉了很多(xiong)萌(hai)新(zi),服务器里很是热闹。

然后呢,limenge同学一直想要个大地图,kasora同学就做了个游戏内的大地图。然而呢,服务器这么大,人也不少,聚在一起互相偷东西总是不太好,想找个平原安家也没有明确的方向,于是乎就诞生的搞个全服大地图的想法~

目前利用TOGoS’s Minecraft Map Renderer这个小工具渲染的全地图的俯视图,服务器(219.230.159.13)端配置了计划任务,每小时渲染一次,然后另一台服务器(219.230.153.26)则会每小时将渲染好的内容下载到/var/www/html/minecraft/map内,并配置了VirtualHost,匹配的mcmap.dimension.moe这个域名,所以直接打开mcmap.dimension.moe就可以查看服务器大地图啦~

效果图:(点击可看大图)

Minecraft Server World Map
Minecraft Server World Map

219.230.153.26是一台放在ACM房间的台式机,利用我的上网账户获得了公网IP,实际网络访问速度不是很快,再加上地图很大,所以打开速度可能会有点慢,不过嘛,效果不错就是啦~

]]>
https://archive.ntzyz.io/2015/10/23/big-map-of-the-minecraft-server/feed/ 0
[Minecraft] 随手写了个自动下载与更新器 https://archive.ntzyz.io/2015/09/20/minecraft-download-and-update-tool/ https://archive.ntzyz.io/2015/09/20/minecraft-download-and-update-tool/#respond Sat, 19 Sep 2015 16:32:24 +0000 https://blog.dimension.moe/?p=141 继续阅读[Minecraft] 随手写了个自动下载与更新器]]> 9月20日更新:C#版本完成,具有最简单的错误处理和图形界面(雾草这个只有字的窗口也能叫图形?)


程序很简单,本地一个文件记录本地版本,服务器端有个文件记录最新版本,如果服务器版本高于本地版本,就会自动下载相应的tar更新包,并自动解压,解压后再执行install.bat来完成更新的安装。

当然如果没有找到本地版本的记录文件,就会从服务器上下载一份完整的纯净客户端,然后再按补丁顺序依次安装。

C++的下载文件通过tools文件夹下的wget来实现,解压tar包则是靠的tar来完成。

C#的则是交给HttpWebRequest,HttpWebResponse和SharpZipLib来完成。

代码写得相当随意,基本上想到哪里写到哪里,而且也没有任何错误处理……不要打我最近网络赛和codeforces好多的

顺便求不吐槽英语……

以后会更新错误处理、本地包的安装和图形界面。

[点击此处下载C++版本]
[点击此处下载C#版本]
源码就丢这里一份吧:

#include 
#include 
#include 

const size_t STRINGLENGTH = 512;

const char *HttpVersion = "http://zh.server309.dimension.moe/minecraft/LastVersion.txt";
const char *localVersionFile = "version.txt";
const char *basePackage = "http://zh.server309.dimension.moe/minecraft/base.tar";

bool exist(const char *path);
void download(const char *url, const char *fileName);

int main() {
	//检查游戏是否已经下载完成
	if (!exist(localVersionFile)) {
		//如果用户不想下载,直接退出好了
		puts("Couldn\'t find any game files. \nDo you want to Download it NOW [Y/n]");
		if (toupper(getchar()) != 'Y') {
			puts("Aborting...");
			return 0;
		}
		//下载完整的纯净版游戏
		download(basePackage, "base.tar");
		//下载完了,解压呗
		puts("Download base package finished. Now installing it. It may take a few minutes...");
		system("tools\\tar xf base.tar");
	}
	//Now we are sure that we have the base game files. we just need to 
	//check if there's any update for mods and configure files
	
	//First, we need to get the newest version of game on server.
	download(HttpVersion, "tools\\ServerVerion.txt");
	FILE *serverVersion, *localVersion;
	int Sversion, Lversion;
	serverVersion = fopen("tools\\ServerVerion.txt", "rb");
	fscanf(serverVersion, "1.%d.0", &Sversion);
	fclose(serverVersion);
	localVersion = fopen("version.txt", "rb");
	fscanf(localVersion, "1.%d.0", &Lversion);
	fclose(localVersion);
	//check if we are up-to-date.
	if (Lversion == Sversion) {
		//the version is the same.
		puts("Your game is up-to-date!\nJust click mclauncher and enjoy it!");
		system("pause");
		return 0;
	}
	else {
		//we need to update the game, step-by-step accoding to the version.
		printf("We need to update some files to play the game.\n"
			"Local game version:  1.%d.0\n"
			"Server game version: 1.%d.0\n",
			Lversion, Sversion
		);
		for (int i = Lversion; i < Sversion; i++) {
			printf("Updating 1.%d.0 to 1.%d.0... ", i, i + 1);
			char updatePackage[STRINGLENGTH];
			//Download update package.
			sprintf(updatePackage, "http://zh.server309.dimension.moe/minecraft/update-%d.tar", i + 1);
			download(updatePackage, "update.tar");
			//decompress the package.
			system("tools\\tar xf update.tar");
			//call the update script to install the update package.
			system("\"update\\install.bat\"");
			//remove the packages
			system("del update.tar");
			system("del /F /A /Q update");
			localVersion = fopen("version.txt", "rb+");
			fprintf(localVersion, "1.%d.0", i + 1);
			fclose(localVersion);
			puts("finished!");
		}
	}
	system("pause");
	return 0;
}

void download(const char *url, const char *fileName) {
	char command[STRINGLENGTH];
	sprintf(command, "tools\\wget -O %s %s > tools\\logs\\wget.log", fileName, url);
	system(command);
}

bool exist(const char *path) {
	FILE *fp = fopen(path, "r");
	if (!fp)
		return false;
	else {
		fclose(fp);
		return true;
	}
}

C#版本源码:

using System;
using System.Windows.Forms;
using System.IO;
using System.Net;
using System.Threading;
using ICSharpCode.SharpZipLib.Tar;
using System.Text;

namespace Updater
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void download(string url, string fileName)
        {
            HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
            HttpWebResponse response = request.GetResponse() as HttpWebResponse;
            Stream responseStream = response.GetResponseStream();
            Stream stream = new FileStream(fileName, FileMode.Create);
            byte[] buffer = new byte[1024];
            int size = responseStream.Read(buffer, 0, (int)buffer.Length);
            while (size > 0)
            {
                stream.Write(buffer, 0, size);
                size = responseStream.Read(buffer, 0, (int)buffer.Length);
            }
            stream.Close();
            responseStream.Close();
        }
        public void DeleteFolder(string deleteDirectory)
        {
            if (Directory.Exists(deleteDirectory))
            {
                foreach (string deleteFile in Directory.GetFileSystemEntries(deleteDirectory))
                {
                    if (File.Exists(deleteFile))
                        File.Delete(deleteFile);
                    else
                        DeleteFolder(deleteFile);
                }
                Directory.Delete(deleteDirectory);
            }
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            Control.CheckForIllegalCrossThreadCalls = false;
            Thread th = new Thread(main);
            th.IsBackground = true;
            th.Start();
        }
        private void main()
        {
            textBox1.Text = "Minecraft Updater 1.1\r\n";
            textBox1.Text += "检测是否已下载游戏\r\n";
            if (!File.Exists("Version.txt"))
            {
                textBox1.Text += "未找到本地游戏,开始下载。这可能需要一段时间……\r\n";
                try
                {
                    if (File.Exists("base.tar"))
                        textBox1.Text += "找到本地离线安装包,跳过下载\r\n";
                    else
                        download("http://zh.server309.dimension.moe/minecraft/base.tar", "base.tar");
                }
                catch (Exception ex)
                {
                    textBox1.Text += "发生错误:" + ex.Message + "\r\n程序已终止!";
                    MessageBox.Show("发生错误,在下载纯净版客户端:" + ex.Message + "\r\n程序已终止!");
                    Application.Exit();
                }
                textBox1.Text += "下载纯净版完成,开始解压缩……\r\n";
                try
                {
                    TarArchive package = TarArchive.CreateInputTarArchive(File.OpenRead("base.tar"));
                    package.ExtractContents(Environment.CurrentDirectory);
                    package.Close();
                }
                catch (Exception ex)
                {
                    textBox1.Text += "发生错误:" + ex.Message + "\r\n程序已终止!";
                    MessageBox.Show("发生错误,在解压缩纯净版客户端:" + ex.Message + "\r\n程序已终止!");
                    Application.Exit();
                }
                textBox1.Text += "解压缩完成,进入检查更新阶段……\r\n";
                File.Delete("base.tar");
            }
            textBox1.Text += "获取服务器端最新版本号……\r\n";
            string serverVersion, localVersion;
            try
            {
                HttpWebRequest request = WebRequest.Create("http://zh.server309.dimension.moe/minecraft/LastVersion.txt") as HttpWebRequest;
                request.Method = "GET";
                HttpWebResponse response = request.GetResponse() as HttpWebResponse;
                var reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
                serverVersion = reader.ReadToEnd();
                textBox1.Text += "服务器端版本为:" + serverVersion + "\r\n";
                textBox1.Text += "获取本地版本号……\r\n";
                StreamReader sr = new StreamReader("Version.txt", Encoding.Default);
                localVersion = sr.ReadLine();
                textBox1.Text += "本地版本为:" + localVersion + "\r\n";
                if (localVersion == serverVersion)
                {
                    textBox1.Text += "本地游戏已是最新版,无需安装更新。\r\n";
                    return;
                }
                string[] SVersion = serverVersion.Split('.');
                string[] LVersion = localVersion.Split('.');
                for (int i = Convert.ToInt32(LVersion[1]); i != Convert.ToInt32(SVersion[1]); i++)
                {
                    textBox1.Text += "正在下载自1." + i + ".0至1." + (i + 1) + ".0的更新……\r\n";
                    string updatePackageName = "http://zh.server309.dimension.moe/minecraft/update-" + (i + 1) + ".tar";
                    download(updatePackageName, "upgrade.tar");
                    textBox1.Text += "正在解压更新……\r\n";
                    TarArchive package = TarArchive.CreateInputTarArchive(File.OpenRead("upgrade.tar"));
                    package.ExtractContents(Environment.CurrentDirectory);
                    package.Close();
                    textBox1.Text += "正在安装更新\r\n";
                    System.Diagnostics.Process process = new System.Diagnostics.Process();
                    process.StartInfo.CreateNoWindow = false;
                    process.StartInfo.FileName = "update\\install.bat";
                    process.Start();
                    StreamWriter sw = new StreamWriter(File.OpenWrite("Version.txt"), Encoding.Default);
                    sw.WriteLine("1." + (i + 1) + ".0");
                    Thread.Sleep(500);
                    textBox1.Text += "清理更新文件\r\n";
                    DeleteFolder("update");
                    File.Delete("upgrade.tar");
                }
                textBox1.Text += "全部更新安装完成,可以开始游戏了。\r\n";
            }
            catch (Exception ex)
            {
                textBox1.Text += "发生错误:" + ex.Message + "\r\n程序已终止!";
                MessageBox.Show("发生错误,在更新本地客户端:" + ex.Message + "\r\n程序已终止!");
                Application.Exit();
            }
        }
    }
}
]]>
https://archive.ntzyz.io/2015/09/20/minecraft-download-and-update-tool/feed/ 0