在游戏饥荒中,如果已经以离线或者在线模式创建了世界,就无法直接在游戏中切换离线与在线模式(比如有的时候想在没有网络的环境下玩之前创建的在线模式世界)。但是可以通过修改存档文件来实现。

存档路径: C:\Users\xxx\Documents\Klei\DoNotStarveTogether\xxxxxxx\

1. 修改配置文件

  • 进入需要修改的存档文件夹,比如Cluster_1。
  • 打开Master/save/shardindex,修改server选项中的online_modetrue(在线模式)或false(离线模式)。
  • 打开Caves/save/shardindex,同上修改server选项中的online_mode
  • 注意根目录下的cluster.ini文件中的offline_cluster的值对最终的结果没有影响,改不改都可以。

2. 同步角色数据

当启动游戏后,由于在线模式和离线模式使用的是不同的账号数据,所以直接进入游戏会失去之前的角色数据。所以第一次切换后会提示创建新的角色。但是我们可以手动把之前的角色数据复制过来。

进入到 C:\Users\xxx\Documents\Klei\DoNotStarveTogether\xxxxxxx\Cluster_xx\Master\save\session\xxxxxxx\ 目录下,可以找到一个英文+数字的文件夹(只有进入过游戏才会有,比如你进了地表世界,但没去过洞穴,那么 Caves\save\session\xxxxxxx\ 目录下就没有这个文件夹),该文件夹名称和你的账号是相关的,你可以看到所有的世界里这个文件夹的名称都是一样的。这里假设这个叫 ABC123。此时你通过离线模式启动服务器,会提示你创建角色,这时你会发现 Master\save\session\xxxxxxx\ 目录下多了一个文件夹,这个就是你离线模式的账号数据,假设叫 DEF456。此时你只需要把 ABC123 目录下的所有文件复制到 DEF456 目录下,覆盖掉里面的文件,就可以在离线模式下继续使用之前在线模式的角色数据了。对于 Cave 洞穴世界也是同样的操作。

同理,如果你想从离线模式切换回在线模式,也是一样的操作。只需要把之前离线模式的账号数据复制到在线模式的账号数据目录下覆盖掉就可以了。

3. 脚本实现

我把上述操作写成了一个 python 脚本,方便以后使用:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
饥荒存档在线/离线模式切换工具
用于切换饥荒联机版存档的在线模式
"""

import sys
import configparser
from pathlib import Path
from datetime import datetime
import re
import shutil
import os


# 解决Windows命令行颜色问题
if sys.platform == "win32":
    os.system('')


# 存档位置: C:\Users\xxx\Documents\Klei\DoNotStarveTogether\{user_id}\
user_id = '1234567890' # 替换成你的用户id

# 在线模式和离线模式的session_id
# C:\Users\xxx\Documents\Klei\DoNotStarveTogether\{user_id}\Cluster_xx\Master\save\session\xxxxxxx\{session_id}\
# 要获取这两个id,可以分别创建一个在线模式存档和一个离线模式存档,并进入一次游戏,然后进入到上述路径下查看对应的session_id文件夹名称,复制过来即可。所有存档的这两个session_id都是一样的。
online_session_id = 'ABCD123456' # 替换成你的在线模式session_id
offline_session_id = 'CDEFG123456789' # 替换成你的离线模式session_id


class Color:
    RED = '\033[91m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    CYAN = '\033[96m'
    RESET = '\033[0m'


class ColorStr:
    ONLINE = f"{Color.GREEN}在线{Color.RESET}"
    OFFLINE = f"{Color.RED}离线{Color.RESET}"
    UNKNOWN = f"{Color.YELLOW}未知{Color.RESET}"


def color_str(text, color):
    return f"{color}{text}{Color.RESET}"


def align(text, width=20, fillchar=' '):
    """
    使中英文混合字符串对齐
    """
    # 移除颜色码对长度的影响
    temp_text = re.sub(r'\033\[\d+m', '', text)
    # 计算实际字符宽度(非ASCII字符即中文按2计算)
    actual_len = sum(2 if ord(c) > 127 else 1 for c in temp_text)
    # 计算需要填充的空格数
    return text + fillchar * (width - actual_len)


class DSTSaveManager:
    def __init__(self):
        if sys.platform == 'win32':
            base_path = Path.home() / 'Documents' / 'Klei' / 'DoNotStarveTogether' / user_id
        else:
            base_path = Path.home() / '.klei' / 'DoNotStarveTogether'

        self.base_path = base_path

        if not self.base_path.exists():
            print(f"错误:找不到饥荒存档目录")
            print(f"预期路径:{self.base_path}")
            sys.exit(1)

    def get_saves(self):
        saves = []

        for cluster_dir in self.base_path.iterdir():
            if not cluster_dir.is_dir():
                continue

            if cluster_dir.name.startswith('.'):
                continue

            cluster_ini = cluster_dir / 'cluster.ini'
            if not cluster_ini.exists():
                continue

            config = configparser.ConfigParser()
            config.read(cluster_ini, encoding='utf-8')

            cluster_name = config.get('NETWORK', 'cluster_name', fallback=cluster_dir.name)
            mod_time = datetime.fromtimestamp(cluster_dir.stat().st_mtime)
            online_mode = self.get_online_mode(cluster_dir)

            saves.append({
                'path': cluster_dir,
                'name': cluster_name,
                'folder': cluster_dir.name,
                'mod_time': mod_time,
                'online_mode': online_mode
            })

        saves.sort(key=lambda x: x['mod_time'], reverse=True)
        return saves

    def get_online_mode(self, cluster_dir):
        shardindex_path = cluster_dir / 'Master' / 'save' / 'shardindex'

        if not shardindex_path.exists():
            return None

        try:
            with open(shardindex_path, 'r', encoding='utf-8') as f:
                content = f.read()

            match = re.search(r'online_mode\s*=\s*(true|false)', content)
            if match:
                return match.group(1) == 'true'
            return None
        except:
            return None

    def set_online_mode(self, cluster_dir, online_mode):
        shard_paths = [
            cluster_dir / 'Master' / 'save' / 'shardindex',
            cluster_dir / 'Caves' / 'save' / 'shardindex'
        ]

        success_count = 0
        failed_paths = []

        for shard_path in shard_paths:
            if not shard_path.exists():
                continue

            try:
                with open(shard_path, 'r', encoding='utf-8') as f:
                    content = f.read()

                new_value = 'true' if online_mode else 'false'
                new_content = re.sub(
                    r'online_mode\s*=\s*(true|false)',
                    f'online_mode={new_value}',
                    content
                )

                with open(shard_path, 'w', encoding='utf-8') as f:
                    f.write(new_content)

                success_count += 1

            except Exception as e:
                failed_paths.append(str(shard_path))

        return success_count, failed_paths

    def get_session_root_path(self, cluster_dir, shard):
        temp_path = cluster_dir / shard / 'save' / 'session'
        if not temp_path.exists():
            raise FileNotFoundError(f"找不到路径: {temp_path}")
        subdirs = [d for d in temp_path.iterdir() if d.is_dir()]
        if not subdirs:
            raise FileNotFoundError(f"找不到子目录: {temp_path}")
        return subdirs[0]

    def backup_session(self, session_path, session_id):
        backups_root = session_path.parent / "backups"
        backup_path = backups_root / session_id

        backups_root.mkdir(exist_ok=True)

        if backup_path.exists():
            shutil.rmtree(backup_path)

        shutil.copytree(session_path, backup_path)

        return backup_path

    def copy_one_shard(self, cluster_dir, shard, src_id, dst_id):
        try:
            session_root = self.get_session_root_path(cluster_dir, shard)
        except FileNotFoundError as e:
            print(f"错误: {e}")
            return

        src_path = session_root / src_id
        dst_path = session_root / dst_id

        print(f"\n{session_root}:")
        print(f"\t{src_path.name} -> {dst_path.name}")

        if not src_path.exists():
            print("跳过(源不存在):", src_path)
            return

        if dst_path.exists():
            if dst_path.stat().st_mtime > src_path.stat().st_mtime:
                print("⚠ 目标存档较新")
                confirm = input("仍然覆盖?(y/n): ").strip().lower()
                if confirm != 'y':
                    print("已跳过")
                    return

            backup_path = self.backup_session(dst_path, dst_id)
            print(f"已备份到: {backup_path}")

            shutil.rmtree(dst_path)

        shutil.copytree(src_path, dst_path)
        print(f"{shard} 人物存档同步成功")

    def copy_character_save(self, cluster_dir, to_online):
        if to_online:
            src_id = offline_session_id
            dst_id = online_session_id
            direction = f"{color_str('离线', Color.RED)}{color_str('在线', Color.GREEN)}"
        else:
            src_id = online_session_id
            dst_id = offline_session_id
            direction = f"{color_str('在线', Color.GREEN)}{color_str('离线', Color.RED)}"

        print("\n人物存档复制")
        print(f"方向: {direction}")

        self.copy_one_shard(cluster_dir, "Master", src_id, dst_id)
        self.copy_one_shard(cluster_dir, "Caves", src_id, dst_id)


    def display_saves(self, saves):
        print("\n" + "=" * 100)
        # print(f"{'序号':<6} {'文件夹名':<20} {'世界名':<25} {'在线/离线':<10} {'修改时间'}")
        print(f"{align('序号', 6)} {align('文件夹名', 20)} {align('世界名', 25)} {align('在线/离线', 15)} {'修改时间'}")
        print("=" * 100)

        for idx, save in enumerate(saves, 1):
            if save['online_mode'] is True:
                mode_str = ColorStr.ONLINE
            elif save['online_mode'] is False:
                mode_str = ColorStr.OFFLINE
            else:
                mode_str = ColorStr.UNKNOWN

            mod_time_str = save['mod_time'].strftime('%Y-%m-%d %H:%M:%S')

            # print(f"{idx:<6} {save['folder']:<20} {save['name']:<25} {mode_str:<10} {mod_time_str}")
            print(f"{align(str(idx), 6)} {align(save['folder'], 20)} {align(save['name'], 25)} {align(mode_str, 15)} {mod_time_str}")

        print("=" * 100)
        print(f"\n共找到 {len(saves)} 个存档")

    def run(self):
        print("饥荒存档在线/离线模式切换工具")
        print(f"存档目录:{self.base_path}\n")

        while True:
            saves = self.get_saves()
            self.display_saves(saves)

            user_input = input("请输入序号(q退出): ").strip()

            if user_input.lower() == 'q' or user_input == 'quit':
                break
            
            if not user_input.isdigit():
                print("请输入有效的序号")
                continue

            try:
                idx = int(user_input) - 1
                selected = saves[idx]

                new_mode = not selected['online_mode']

                if selected['online_mode'] is None:
                    print("无法切换(未知当前模式)")
                    continue
                
                print(f"\n{selected['folder']}({selected['name']}) 从 {ColorStr.ONLINE if selected['online_mode'] else ColorStr.OFFLINE} 切换到 {ColorStr.ONLINE if new_mode else ColorStr.OFFLINE} 模式")

                confirm = input("确认切换?(y/n): ").strip().lower()
                if confirm != 'y':
                    continue

                success_count, _ = self.set_online_mode(selected['path'], new_mode)

                if success_count:
                    print("✓ 模式切换成功")

                    print("\n是否同步人物存档?")
                    master_session_root = self.get_session_root_path(selected['path'], 'Master')
                    cave_session_root = self.get_session_root_path(selected['path'], 'Caves') if (selected['path'] / 'Caves').exists() else None
                    print(f"你也可以稍后手动同步存档,进入到下面目录:")
                    print(f"  {master_session_root}")
                    if cave_session_root:
                        print(f"  {cave_session_root}")
                    print(f"{color_str(offline_session_id if new_mode else online_session_id, Color.RED)}目录的内容复制到{color_str(online_session_id if new_mode else offline_session_id, Color.GREEN)}目录下")
                    copy_confirm = input("(y/n): ").strip().lower()

                    if copy_confirm == 'y':
                        self.copy_character_save(
                            selected['path'],
                            to_online=new_mode
                        )

                    print("\n⚠ 请重启游戏以加载存档状态变更\n")

            except Exception as e:
                print("错误:", e)


def main():
    DSTSaveManager().run()


if __name__ == '__main__':
    main()