这几天阿里云通过学生认证给了300免费额度,于是白嫖了几个月的2核2G轻量服务器。
心血来潮又拿来搭了饥荒服务器,然后写了一段python程序,省的每次都得从头再来。
应该已经有类似的快速搭建工具了,但是还是闲着没事自己写了一个。
参考:搭建饥荒服务器(2024.1) - 知乎 (zhihu.com)
修改:2.21-----修复了上传存档时解压报错的问题
目录
前提条件
1. linux ubuntu服务器,内存不少于2G(勉强可以5个人)。
2. 服务器设置为通过密码登录。
3. 一个本地建立的完整饥荒存档,已经设置好了各类参数和mod。
编写服务器启动程序
以下代码命名为startup.sh
#!/bin/bash
steamcmd_dir="$HOME/steamcmd"
install_dir="$HOME/dontstarvetogether_dedicated_server"
cluster_name="Cluster_1"
dontstarve_dir="$HOME/.klei/DoNotStarveTogether"
function fail()
{
echo Error: "$@" >&2
exit 1
}
function check_for_file()
{
if [ ! -e "$1" ]; then
fail "Missing file: $1"
fi
}
cd "$steamcmd_dir" || fail "Missing $steamcmd_dir directory!"
check_for_file "steamcmd.sh"
check_for_file "$dontstarve_dir/$cluster_name/cluster.ini"
check_for_file "$dontstarve_dir/$cluster_name/cluster_token.txt"
check_for_file "$dontstarve_dir/$cluster_name/Master/server.ini"
check_for_file "$dontstarve_dir/$cluster_name/Caves/server.ini"
check_for_file "$install_dir/bin"
cd "$install_dir/bin" || fail
run_shared=(./dontstarve_dedicated_server_nullrenderer)
run_shared+=(-console)
run_shared+=(-cluster "$cluster_name")
run_shared+=(-monitor_parent_process $$)
run_shared+=(-shard)
"${run_shared[@]}" Caves | sed 's/^/Caves: /' &
"${run_shared[@]}" Master | sed 's/^/Master: /'
服务器搭建主程序
以下代码随便命名,以.py结尾, 导入的库没有的话就自己pip下载吧。
修改开头的user host token password等参数,然后和上一个程序放在同一个目录下。
from fabric import Connection
import shutil
import re
import os
import time
import tkinter as tk
from tkinter import filedialog,messagebox
user = "root" #设置你的登录名,最好是以root方式,否则可能出问题
host = "" #设置服务器IP地址
token="" #设置你的饥荒服务器token,在游戏官网获取,详见参考
password="" #设置服务器密码
swap_p=30 #设置swappiness,内存比较大可以小一点
swap_size=2048 #设置swap大小,以M为单位
global Cluster_name
Cluster_name=""
def check_swap(c):
# 利用run方法直接执行传入的命令
print("\n # 当前swap分区:")
output=c.run('swapon').stdout
print("\n # 当前swap分区开始使用的内存占用比:")
output=c.run('cat /proc/sys/vm/swappiness').stdout
def change_swappiness(c,swap_p):
output=c.run('cat /etc/sysctl.conf', hide=True).stdout
if "vm.swappiness" in output:
#去除原来的vm.swappiness行
output=c.run('sed -i "/vm.swappiness/d" /etc/sysctl.conf', hide=True).stdout
#在文件开头添加新的vm.swappiness行
output=c.run('sed -i "1i vm.swappiness = '+str(swap_p)+'" /etc/sysctl.conf', hide=True).stdout
#重新加载sysctl.conf文件
output=c.run('sysctl -p', hide=True).stdout
def change_swapsize(c,swap_size):
output=c.run('sudo swapoff -v /mnt/swap', hide=True).stdout
output=c.run('sudo dd if=/dev/zero of=/mnt/swap bs=1M count='+str(swap_size)).stdout
output=c.run('sudo chmod 600 /mnt/swap').stdout
output=c.run('sudo mkswap /mnt/swap').stdout
output=c.run('sudo swapon /mnt/swap').stdout
def check_server_f(c):
# 利用run方法直接执行传入的命令
output=c.run('ps -ef | grep dontstarve', hide=True).stdout
if "dontstarve_dedicated_server_nullrenderer" in output:
print('\n # 服务器已经启动')
return True
else:
print('\n # 服务器未启动')
return False
def check_server(c):
# 利用run方法直接执行传入的命令
output=c.run('ps -ef | grep dontstarve', hide=True).stdout
if "dontstarve_dedicated_server_nullrenderer" in output:
return True
else:
return False
def restart_server(c):
stop_server(c)
while check_server(c):
time.sleep(1)
pass
start_server(c)
def start_server(c):
# 利用run方法直接执行传入的命令
if check_server(c):
print('\n # 服务器已经启动,无法再次启动')
return
output=c.run('nohup ~/startup.sh>root.log 2>&1 &').stdout
print('\n # 服务器已启动')
def stop_server(c):
if not check_server(c):
print('\n # 服务器未运行,无法停止')
return
# 利用run方法直接执行传入的命令
output=c.run('pkill -f dontstarve_dedicated_server_nullrenderer').stdout
print('\n # 服务器已停止')
def update_server(c):
# 利用run方法直接执行传入的命令
output=c.run('~/steamcmd/steamcmd.sh +force_install_dir ~/dontstarvetogether_dedicated_server +login anonymous +app_update 343050 validate +quit').stdout
print('\n # 服务器已更新,开始重启')
restart_server(c)
def backup_cluster(c):
# 利用run方法直接执行传入的命令
output=c.run('cd ~/.klei/DoNotStarveTogether && tar -cvzf Cluster_1.tar.gz Cluster_1').stdout
#本地创建backup文件夹
if not os.path.exists('backup'):
os.makedirs('backup')
#下载Cluster_1.tar.gz文件
ctime=time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
#移动Cluster_1.tar.gz文件到~下
output=c.run('mv ~/.klei/DoNotStarveTogether/Cluster_1.tar.gz ~').stdout
print('\n # 开始下载存档')
c.get('Cluster_1.tar.gz', "backup/Cluster_1_"+ctime+".tar.gz")
print('\n # 存档已下载,正在解压...')
#本地解压Cluster_1.tar.gz文件
shutil.unpack_archive("backup/Cluster_1_"+ctime+".tar.gz", "backup/Cluster_1_"+ctime)
#删除本地的Cluster_1.tar.gz文件
os.remove("backup/Cluster_1_"+ctime+".tar.gz")
#删除远程服务器上的Cluster_1.tar.gz文件
output=c.run('rm -rf ~/Cluster_1.tar.gz').stdout
print('\n # 存档备份完成')
def update_cluster(c):
stop_server(c)
#在本地Cluster_1文件夹中创建一个txt文件,写入token
with open(Cluster_name+'/cluster_token.txt', 'w') as f:
f.write(token)
# 利用run方法直接执行传入的命令
shutil.make_archive('Cluster_1', 'gztar', Cluster_name)
#以文本形式打开Cluster_1\Master中的modoverrides.lua
with open(Cluster_name+'/Master/modoverrides.lua', 'r') as f:
modoverrides = f.read()
mods=re.findall(r'"workshop-(.*?)"]', modoverrides)
output=c.run('mkdir -p ~/.klei/DoNotStarveTogether/Cluster_1').stdout
output=c.put('Cluster_1.tar.gz', "Cluster_1.tar.gz")
output=c.run('tar -xvzf Cluster_1.tar.gz -C ~/.klei/DoNotStarveTogether/Cluster_1').stdout
output=c.run('rm -rf Cluster_1.tar.gz').stdout
os.remove('Cluster_1.tar.gz')
os.remove(Cluster_name+'/cluster_token.txt')
output=c.run('rm -rf Cluster_1.tar.gz').stdout
#清空重建dedicated_server_mods_setup.lua
output=c.run('echo "" > ~/dontstarvetogether_dedicated_server/mods/dedicated_server_mods_setup.lua').stdout
#在dedicated_server_mods_setup.lua文件后添加行
for mod in mods:
#添加行
output=c.run(f'echo "ServerModSetup(\"{mod}\")" >> ~/dontstarvetogether_dedicated_server/mods/dedicated_server_mods_setup.lua').stdout
while check_server(c):
time.sleep(1)
pass
start_server(c)
print('\n # 存档已上传/更新')
def init_server(c):
# 利用run方法直接执行传入的命令
#检查是否存在startup.sh文件
output=c.run('ls', hide=True).stdout
if "startup.sh" in output:
return
output=c.run('sudo apt -y update').stdout
output=c.run('sudo apt-get install software-properties-common -y').stdout
output=c.run('sudo add-apt-repository multiverse').stdout
output=c.run('sudo dpkg --add-architecture i386').stdout
output=c.run('sudo apt install libstdc++6 libgcc1 libcurl4-gnutls-dev:i386 lib32z1 -y').stdout
output=c.run('sudo apt install libstdc++6 -y').stdout
output=c.run('sudo apt install lib32stdc++6 -y').stdout
output=c.run('mkdir ~/steamcmd').stdout
output=c.run('wget https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz').stdout
output=c.run('tar -xvzf steamcmd_linux.tar.gz -C ~/steamcmd').stdout
output=c.run('rm -rf steamcmd_linux.tar.gz').stdout
output=c.run('~/steamcmd/steamcmd.sh +force_install_dir ~/dontstarvetogether_dedicated_server +login anonymous +app_update 343050 validate +quit').stdout
output=c.run('dd if=/dev/zero of=/mnt/swap bs=1M count='+str(swap_size)).stdout
output=c.run('sudo chmod 600 /mnt/swap').stdout
output=c.run('mkswap /mnt/swap').stdout
output=c.run('swapon /mnt/swap').stdout
output=c.run('echo "/swapfile swap swap defaults 0 0" >> /etc/fstab').stdout
output=c.run('echo "vm.swappiness = '+str(swap_p)+'" >> /etc/sysctl.conf').stdout
output=c.run('sysctl -p', hide=True).stdout
output=c.run('sudo apt-get install dos2unix -y').stdout
output=c.put('startup.sh', "startup.sh")
output=c.run('dos2unix startup.sh').stdout
output=c.run('sudo chmod u+x startup.sh').stdout
def check_cluster(c):
#检查文件是否存在
try:
output=c.run('ls ~/.klei/DoNotStarveTogether/Cluster_1', hide=True).stdout
if "cluster.ini" not in output:
return
except:
return
# 读取~/.klei/DoNotStarveTogether/Cluster_1/cluster.ini文件内容
output=c.run('cat ~/.klei/DoNotStarveTogether/Cluster_1/cluster.ini | iconv -c -f UTF-8 -t GBK', hide=True).stdout
# 匹配cluster.ini文件中的[]=[]内容,返回一个字典,key为[],value为[]
cluster_ini = re.findall(r'(.*?) = (.*?)\n', output)
# 将cluster_ini转为字典
cluster_ini = dict(cluster_ini)
return cluster_ini
def update_cluster_ini(c,cluster_ini):
#修改cluster.ini文件
output=c.run('sed -i "s/cluster_name = .*/cluster_name = '+cluster_ini['cluster_name']+'/" ~/.klei/DoNotStarveTogether/Cluster_1/cluster.ini', hide=True).stdout
output=c.run('sed -i "s/cluster_password = .*/cluster_password = '+cluster_ini['cluster_password']+'/" ~/.klei/DoNotStarveTogether/Cluster_1/cluster.ini', hide=True).stdout
output=c.run('sed -i "s/cluster_description = .*/cluster_description = '+cluster_ini['cluster_description']+'/" ~/.klei/DoNotStarveTogether/Cluster_1/cluster.ini', hide=True).stdout
output=c.run('sed -i "s/max_players = .*/max_players = '+cluster_ini['max_players']+'/" ~/.klei/DoNotStarveTogether/Cluster_1/cluster.ini', hide=True).stdout
output=c.run('sed -i "s/game_mode = .*/game_mode = '+cluster_ini['game_mode']+'/" ~/.klei/DoNotStarveTogether/Cluster_1/cluster.ini', hide=True).stdout
def check_cluster_f(c):
cluster_ini=check_cluster(c)
print('\n # 当前存档信息-----------------')
print(' 名称:',cluster_ini['cluster_name'])
print(' 密码:',cluster_ini['cluster_password'])
print(' 描述:',cluster_ini['cluster_description'])
print(' 最大玩家数:',cluster_ini['max_players'])
print(' 模式:',cluster_ini['game_mode'])
def update_cluster_ini_f(newindow,c,cluster_ini,option,new_value):
#修改时名称、模式、最大玩家不能为空字符
if option=="cluster_name" and new_value=="":
#弹窗提示输入不合法
messagebox.showinfo('提示','输入不合法')
return
elif option=="game_mode" and new_value=="":
#弹窗提示输入不合法
messagebox.showinfo('提示','输入不合法')
return
elif option=="max_players" and new_value=="":
#弹窗提示输入不合法
messagebox.showinfo('提示','输入不合法')
return
elif option=="max_players" and new_value!="":
if new_value.isdigit():
new_value=int(new_value)
if new_value<0:
#弹窗提示输入不合法
messagebox.showinfo('提示','输入不合法')
return
new_value=str(new_value)
else:
#弹窗提示输入不合法
messagebox.showinfo('提示','输入不合法')
return
cluster_ini[option]=new_value
update_cluster_ini(c,cluster_ini)
print('\n # 存档信息已更新,重启后生效。')
#关闭窗口
newindow.destroy()
def change_swapsize_f(newindow,c,swap_size):
#检查是否为整数
if swap_size.isdigit():
swap_size=int(swap_size)
if swap_size<0:
#弹窗提示输入不合法
messagebox.showinfo('提示','输入不合法')
return
else:
#弹窗提示输入不合法
messagebox.showinfo('提示','输入不合法')
return
change_swapsize(c,swap_size)
#关闭窗口
newindow.destroy()
def change_swappiness_f(newindow,c,swap_p):
#检查是否为整数
if swap_p.isdigit():
swap_p=int(swap_p)
if swap_p<0 or swap_p>100:
#弹窗提示输入不合法
messagebox.showinfo('提示','输入不合法')
return
else:
#弹窗提示输入不合法
messagebox.showinfo('提示','输入不合法')
return
change_swappiness(c,swap_p)
#关闭窗口
newindow.destroy()
def select_cluster_f(newindow):
#弹出选择文件夹对话框
global Cluster_name
Cluster_name = filedialog.askdirectory()
#更新label显示的内容
newindow.children['!label']['text']=Cluster_name
def update_cluster_f(c,newindow):
#检查是否选择了存档文件夹
if Cluster_name=="":
#弹窗提示输入不合法
messagebox.showinfo('提示','请选择存档文件夹')
return
#检查选择的存档文件夹是否存在cluster.ini文件
if not os.path.exists(Cluster_name+'/Master/modoverrides.lua'):
#弹窗提示输入不合法
messagebox.showinfo('提示','存档文件夹不合法')
return
update_cluster(c)
#关闭窗口
newindow.destroy()
def on_update_cluster(c):
#弹窗要求用户选择存档文件夹
newindow = tk.Toplevel(root)
#newindow.attributes('-topmost', 1)
newindow.title("上传/更新存档")
newindow.grab_set()
tk.Button(newindow, text='选择存档文件夹', command=lambda: select_cluster_f(newindow)).grid(row=0, column=0,padx=10,pady=10)
#显示选择的存档文件夹,文本右对齐,加边框
tk.Label(newindow, text=Cluster_name,width=30,anchor='e',relief='groove').grid(row=0, column=1,padx=10,pady=10)
#显示确认提醒,告知用户上传/更新存档会停止服务器并覆盖原存档
tk.Label(newindow, text="请注意上传/更新存档会停止服务器并覆盖原存档!",fg='red').grid(row=1, column=0,padx=10,pady=10,columnspan=2)
#确认按钮
tk.Button(newindow, text="确定", command=lambda: update_cluster_f(c,newindow)).grid(row=2, column=0,padx=10,pady=10)
#取消按钮
tk.Button(newindow, text="取消", command=newindow.destroy).grid(row=2, column=1,padx=10,pady=10)
# 更新窗口,以确保它已经被渲染,这样我们才能获取它的大小
newindow.update_idletasks()
# 计算新窗口的位置,使其位于主窗口的中心
width = newindow.winfo_width()
height = newindow.winfo_height()
x = root.winfo_x() + (root.winfo_width() // 2) - (width // 2)
y = root.winfo_y() + (root.winfo_height() // 2) - (height // 2)
# 设置新窗口的位置
newindow.geometry(f"+{x}+{y}")
newindow.mainloop()
def on_change_cluster_info(c):
ini = check_cluster(c)
# 弹窗一个新的窗口,要求用户在下拉框中选择更改项并输入更改值,不使用simpledialog.askstring是因为它只能输入字符串
newindow = tk.Toplevel(root)
#newindow.attributes('-topmost', 1)
newindow.title("更改存档信息")
newindow.grab_set()
tk.Label(newindow, text="选择更改项:").grid(row=0, column=0,padx=10,pady=10)
option = tk.StringVar(newindow)
option.set("cluster_name")
tk.OptionMenu(newindow, option, "cluster_name", "cluster_password", "cluster_description", "max_players", "game_mode").grid(row=0, column=1,padx=10,pady=10)
tk.Label(newindow, text="输入新的值:").grid(row=1, column=0,padx=10,pady=10)
new_value = tk.Entry(newindow)
new_value.grid(row=1, column=1,padx=10,pady=10)
tk.Button(newindow, text="确定", command=lambda: update_cluster_ini_f(newindow,c, ini, option.get(), new_value.get())).grid(row=2, column=0,padx=10,pady=10)
tk.Button(newindow, text="取消", command=newindow.destroy).grid(row=2, column=1,padx=10,pady=10)
# 更新窗口,以确保它已经被渲染,这样我们才能获取它的大小
newindow.update_idletasks()
# 计算新窗口的位置,使其位于主窗口的中心
width = newindow.winfo_width()
height = newindow.winfo_height()
x = root.winfo_x() + (root.winfo_width() // 2) - (width // 2)
y = root.winfo_y() + (root.winfo_height() // 2) - (height // 2)
# 设置新窗口的位置
newindow.geometry(f"+{x}+{y}")
newindow.mainloop()
def on_change_swapsize(c):
newindow = tk.Toplevel(root)
#newindow.attributes('-topmost', 1)
newindow.title("更改swap分区大小")
newindow.grab_set()
tk.Label(newindow, text="输入新的swap分区大小(M):").grid(row=0, column=0,padx=10,pady=10)
swap_size = tk.Entry(newindow)
swap_size.grid(row=0, column=1,padx=10,pady=10)
tk.Button(newindow, text="确定", command=lambda: change_swapsize_f(newindow,c, swap_size.get())).grid(row=1, column=0,padx=10,pady=10)
tk.Button(newindow, text="取消", command=newindow.destroy).grid(row=1, column=1,padx=10,pady=10)
# 更新窗口,以确保它已经被渲染,这样我们才能获取它的大小
newindow.update_idletasks()
# 计算新窗口的位置,使其位于主窗口的中心
width = newindow.winfo_width()
height = newindow.winfo_height()
x = root.winfo_x() + (root.winfo_width() // 2) - (width // 2)
y = root.winfo_y() + (root.winfo_height() // 2) - (height // 2)
# 设置新窗口的位置
newindow.geometry(f"+{x}+{y}")
newindow.mainloop()
def on_change_swappiness(c):
newindow = tk.Toplevel(root)
#newindow.attributes('-topmost', 1)
newindow.title("更改swappiness")
tk.Label(newindow, text="输入新的swappiness(0-100):").grid(row=0, column=0,padx=10,pady=10)
swap_p = tk.Entry(newindow)
swap_p.grid(row=0, column=1,padx=10,pady=10)
tk.Button(newindow, text="确定", command=lambda: change_swappiness_f(newindow,c, swap_p.get())).grid(row=1, column=0,padx=10,pady=10)
tk.Button(newindow, text="取消", command=newindow.destroy).grid(row=1, column=1,padx=10,pady=10)
# 更新窗口,以确保它已经被渲染,这样我们才能获取它的大小
newindow.update_idletasks()
# 计算新窗口的位置,使其位于主窗口的中心
width = newindow.winfo_width()
height = newindow.winfo_height()
x = root.winfo_x() + (root.winfo_width() // 2) - (width // 2)
y = root.winfo_y() + (root.winfo_height() // 2) - (height // 2)
# 设置新窗口的位置
newindow.geometry(f"+{x}+{y}")
newindow.grab_set()
newindow.mainloop()
# 对前文提供的create_app函数进行扩展以包含所有操作
def create_app(root):
root.title('服务器管理工具')
tk.Button(root, text='查看当前存档信息', command=lambda: check_cluster_f(c),width=15).grid(row=0, column=0,padx=10,pady=10)
tk.Button(root, text='备份当前存档', command=lambda: backup_cluster(c),width=15).grid(row=0, column=1,padx=10,pady=10)
tk.Button(root, text='更新/上传存档', command=lambda: on_update_cluster(c),width=15).grid(row=0, column=2,padx=10,pady=10)
tk.Button(root, text='更改存档信息', command=lambda: on_change_cluster_info(c),width=15).grid(row=1, column=0,padx=10,pady=10)
tk.Button(root, text='查看服务器状态', command=lambda: check_server_f(c),width=15).grid(row=1, column=1,padx=10,pady=10)
tk.Button(root, text='更新服务器并重启', command=lambda: update_server(c),width=15).grid(row=1, column=2,padx=10,pady=10)
tk.Button(root, text='启动服务器', command=lambda: start_server(c),width=15).grid(row=2, column=0,padx=10,pady=10)
tk.Button(root, text='停止服务器', command=lambda: stop_server(c),width=15).grid(row=2, column=1,padx=10,pady=10)
tk.Button(root, text='重启服务器', command=lambda: restart_server(c),width=15).grid(row=2, column=2,padx=10,pady=10)
tk.Button(root, text='查看swap分区', command=lambda: check_swap(c),width=15).grid(row=3, column=0,padx=10,pady=10)
tk.Button(root, text='更改swap分区大小', command=lambda: on_change_swapsize(c),width=15).grid(row=3, column=1,padx=10,pady=10) # 示例参数,实际操作需要用户输入
tk.Button(root, text='更改swappiness', command=lambda: on_change_swappiness(c),width=15).grid(row=3, column=2,padx=10,pady=10) # 示例参数,实际操作需要用户输入
if __name__ == '__main__':
c = Connection(host=host, user=user, connect_kwargs={"password": password}, connect_timeout=60)
print(' # 连接成功,开始初始化服务器')
init_server(c)
print(' # 服务器初始化完成')
root = tk.Tk()
#设置root大小不可变
root.resizable(0,0)
create_app(root)
root.mainloop()
#output=c.run('tail -f root.log').stdout
开始搭建
1. 确认IP、密码等信息无误后运行.py主程序,会自动初始化,第一次运行可能需要较长时间。
2. 初始化结束后会弹出tkinter交互框,第一次运行请首先上传存档,选择存档文件夹并确定。
3. 存档上传完成后服务器会自动启动,没有报错的话稍等片刻刷新游戏里的服务器列表,搜索你的服务器名称,直到出现你的专用服务器。
4. 饥荒!启动!
其它
1. 程序还有备份到本地的功能。
2. 每次更新/上传存档都会覆盖原始存档,所以有必要的话请注意备份。
3. 内存较小可能会oom,可以适当增大swap分区大小和swappiness大小。
4. 程序写的比较乱,很多部分是GPT写的,主打一个能用就行。
5. 一个打包好的exe文件,在Windows系统下可以直接运行,不需要python环境,需要首先修改config.ini里的参数(见文章开头)。
4. 其它内容就不说了,如果要临时更改mod,还是先备份到本地、替换里边的mod文件再上传吧,懒得加这个功能了,出现问题请留言。