原型void CustomGameLobbyWindow::startLocalDockerServer()
执行流程
- 读取表单配置
- 校验 Docker 开关与权限
- 保存配置
- 调用 startGameServer
- 更新实例信息与日志
工程说明
UI 层负责配置编排,Docker 命令由独立管理器生成。
关联接口
LocalDockerHostManager::startGameServerCustomGameLobbyWindow::testConnection
查看完整实现
customgamelobbywindow.cpp
void CustomGameLobbyWindow::startLocalDockerServer()
{
if (m_joinMode) {
QMessageBox::information(this, "成员模式", "当前为成员模式,不会启动房主本地游戏服务;可导入/导出配置并验证连接信息。");
return;
}
CustomGameRoomConfig cfg = collectConfig();
if (cfg.roomId.isEmpty() || cfg.docker.imageName.isEmpty()) {
QMessageBox::warning(this, "启动失败", "room_id 和 Docker 镜像不能为空。");
return;
}
saveRoomConfig();
startLobbyControlServerIfNeeded();
QString containerId;
QString msg;
appendLog("正在启动本地游戏服务...");
if (!m_docker->startGameServer(cfg, containerId, msg)) {
appendLog("启动失败:" + msg);
QMessageBox::warning(this, "Docker 启动失败", msg);
return;
}
qint64 now = QDateTime::currentSecsSinceEpoch();
cfg.status = "running";
cfg.locked = true;
cfg.startedAt = now;
cfg.updatedAt = now;
cfg.docker.runningContainerId = containerId;
QString error;
m_storage.saveRoomConfig(cfg, &error);
m_currentConfig = cfg;
populateForm(cfg);
appendLog(msg);
appendLog("本地服务器地址:" + cfg.hostIp + ":" + QString::number(cfg.docker.hostPort));
}
原型bool LocalDockerHostManager::startGameServer(const CustomGameRoomConfig& cfg, QString& outContainerId, QString& outMsg) const
参数
| 参数 | 说明 |
|---|
cfg | 自定义游戏房间配置 |
outContainerId | 输出容器 ID |
outMessage | 输出状态说明 |
执行流程
- 检查 Docker CLI/Engine
- 检查镜像
- 规范容器名
- 构造端口/环境/挂载参数
- 执行 docker run
- 解析容器 ID
工程说明
仅接受结构化配置,避免 UI 直接拼接命令。
关联接口
LocalDockerHostManager::imageExistsLocalDockerHostManager::stopContainerCustomGameRoomConfig
查看完整实现
localdockerhostmanager.cpp
bool LocalDockerHostManager::startGameServer(const CustomGameRoomConfig& cfg,
QString& outContainerId,
QString& outMsg) const
{
outContainerId.clear();
outMsg.clear();
const bool advancedMode =
cfg.docker.advancedMode ||
cfg.gameId.compare("custom_command", Qt::CaseInsensitive) == 0;
if (cfg.roomId.trimmed().isEmpty()) {
outMsg = "房间 ID 为空,无法启动本地游戏服务。";
return false;
}
if (cfg.docker.imageName.trimmed().isEmpty()) {
outMsg = "Docker 镜像名为空,请先选择游戏模板或填写服务镜像。";
return false;
}
if (!advancedMode) {
if (cfg.docker.hostPort < 1 || cfg.docker.hostPort > 65535 ||
cfg.docker.containerPort < 1 || cfg.docker.containerPort > 65535) {
outMsg = "端口范围不合法,请使用 1-65535 之间的端口。";
return false;
}
QString protocol = cfg.docker.protocol.trimmed().toLower();
if (protocol != "tcp" && protocol != "udp") {
outMsg = "协议类型不合法,请填写 tcp 或 udp。";
return false;
}
}
QString dockerMsg;
if (!isDockerAvailable(&dockerMsg)) {
outMsg = "Docker 不可用:" + dockerMsg;
return false;
}
if (!imageExists(cfg.docker.imageName, &dockerMsg)) {
outMsg = "Docker 镜像不存在,请先构建或导入镜像:"
+ cfg.docker.imageName
+ "\n"
+ dockerMsg;
return false;
}
QString containerName = cfg.docker.containerName;
if (containerName.trimmed().isEmpty()) {
containerName = "custom_" + cfg.roomId;
}
containerName = sanitizeDockerName(containerName);
QStringList args;
args << "run"
<< "-d"
<< "--pull=never"
<< "--restart=no"
<< "--name" << containerName
<< "--label" << "p2p.custom_game=1"
<< "--label" << ("p2p.room=" + cfg.roomId);
if (!cfg.docker.memoryLimit.trimmed().isEmpty()) {
args << "--memory" << cfg.docker.memoryLimit.trimmed();
}
if (!cfg.docker.cpuLimit.trimmed().isEmpty()) {
args << "--cpus" << cfg.docker.cpuLimit.trimmed();
}
if (advancedMode) {
for (const QString& mapping : nonEmptyTrimmedLines(cfg.docker.portMappings)) {
args << "-p" << mapping;
}
for (const QString& env : nonEmptyTrimmedLines(cfg.docker.envVars)) {
args << "-e" << env;
}
for (const QString& volume : nonEmptyTrimmedLines(cfg.docker.volumeMounts)) {
args << "-v" << QDir::toNativeSeparators(volume);
}
for (const QString& line : nonEmptyTrimmedLines(cfg.docker.extraDockerArgs)) {
appendSplitArgs(args, line);
}
} else {
QString protocol = cfg.docker.protocol.trimmed().toLower();
if (protocol != "udp") {
protocol = "tcp";
}
QString portMapping = QString("%1:%2/%3")
.arg(cfg.docker.hostPort)
.arg(cfg.docker.containerPort)
.arg(protocol);
args << "-p" << portMapping;
args << "-e" << ("GAME_ID=" + cfg.gameId)
<< "-e" << ("GAME_NAME=" + cfg.gameName)
<< "-e" << ("ROOM_ID=" + cfg.roomId)
<< "-e" << ("MAX_PLAYERS=" + QString::number(cfg.maxPlayers));
const bool isFactorio =
cfg.gameId.compare("factorio", Qt::CaseInsensitive) == 0 ||
cfg.docker.imageName.contains("factorio", Qt::CaseInsensitive);
if (!cfg.docker.saveDir.trimmed().isEmpty()) {
QDir().mkpath(cfg.docker.saveDir);
QString targetMount = isFactorio ? "/factorio" : "/data/saves";
args << "-v" << (QDir::toNativeSeparators(cfg.docker.saveDir) + ":" + targetMount);
}
if (!isFactorio && !cfg.docker.configDir.trimmed().isEmpty()) {
QDir().mkpath(cfg.docker.configDir);
args << "-v" << (QDir::toNativeSeparators(cfg.docker.configDir) + ":/data/config");
}
}
args << cfg.docker.imageName;
if (advancedMode && !cfg.docker.containerCommand.trimmed().isEmpty()) {
for (const QString& line : nonEmptyTrimmedLines(cfg.docker.containerCommand)) {
appendSplitArgs(args, line);
}
}
CommandResult r = runDocker(args, 30000);
if (!r.success) {
outMsg = "Docker 启动失败:" + r.message;
return false;
}
outContainerId = r.stdOut.trimmed();
if (outContainerId.isEmpty()) {
outMsg = "Docker 已返回成功,但未获得 container_id。";
stopContainer(containerName, outMsg);
return false;
}
outMsg = "本地游戏服务已启动:" + outContainerId;
return true;
}
原型bool CustomGameImageManager::loadImageFromTar(const QString& imageTarPath, QString& outMsg) const
参数
| 参数 | 说明 |
|---|
imageTarPath | Docker 镜像归档路径 |
outMsg | 输出执行信息 |
执行流程
- 校验文件存在
- 检查 Docker Engine
- 执行 docker load
- 解析退出状态
- 返回结果
工程说明
镜像归档与普通游戏存档包在入口层分开处理。
关联接口
CustomGameImageManager::checkImageExistsCustomGameLobbyWindow::loadDockerImageFromTar
查看完整实现
customgameimagemanager.cpp
bool CustomGameImageManager::loadImageFromTar(const QString& imageTarPath, QString& outMsg) const
{
if (imageTarPath.trimmed().isEmpty()) {
outMsg = "镜像 tar 路径为空。";
return false;
}
QFileInfo tar(imageTarPath);
if (!tar.exists() || !tar.isFile()) {
outMsg = "镜像 tar 文件不存在:\n" + imageTarPath;
return false;
}
CommandResult r = runDocker(QStringList() << "load" << "-i" << imageTarPath, 120000);
if (r.success) {
outMsg = "镜像导入成功:\n" + r.stdOut;
return true;
}
outMsg = "镜像导入失败:\n" + r.message;
return false;
}
原型void CloudFactorioWindow::sendControlRequest(const QString& cmd, const QJsonObject& extra)
执行流程
- 构造基础 JSON
- 写入 owner/display_name
- 合并附加字段
- 发送到中心服务
工程说明
文件上传下载使用独立 TCP 通道,控制面保持轻量。
关联接口
CloudFactorioWindow::handleControlResponseGameInstanceManager::startCloudFactorioInstance
查看完整实现
cloudfactoriowindow.cpp
void CloudFactorioWindow::sendControlRequest(const QString& cmd, const QJsonObject& extra)
{
if (!m_udp) {
return;
}
QJsonObject req = extra;
req["cmd"] = cmd;
req["owner"] = ownerId();
req["display_name"] = displayName();
req["request_id"] = QString::number(QDateTime::currentMSecsSinceEpoch());
QByteArray data = QJsonDocument(req).toJson(QJsonDocument::Compact);
m_udp->writeDatagram(data, QHostAddress(serverIp()), udpPort());
appendLog("已发送控制请求:" + cmd);
}
原型GameInstanceManager::startCloudFactorioInstance(const std::string& owner, const std::string& displayName)
返回说明
返回 CloudFactorioInstanceInfo。
参数
| 参数 | 说明 |
|---|
owner | 实例所有者 |
displayName | 显示名称 |
saveArchivePath | 可选存档包路径 |
执行流程
- 检查镜像
- 检查并发配额
- 分配 UDP 端口
- 创建实例目录
- 准备配置与存档
- 执行 docker run
- 返回连接信息
工程说明
实例信息包含容器 ID、主机、端口和状态。
关联接口
GameInstanceManager::countCloudFactorioRunningGameInstanceManager::allocatePortGameInstanceManager::stopCloudFactorioInstance
查看完整实现
game_instance_manager.cpp
GameInstanceManager::startCloudFactorioInstance(const std::string& owner,
const std::string& displayName)
{
std::lock_guard<std::mutex> lock(start_mutex);
CloudFactorioInstanceInfo info;
if (owner.empty()) {
info.msg = "owner 为空";
return info;
}
std::string imageMsg;
if (!cloudFactorioImageExists(&imageMsg)) {
info.msg = imageMsg;
return info;
}
int runningCount = countCloudFactorioRunning();
if (runningCount >= CLOUD_FACTORIO_MAX_RUNNING) {
info.msg = "当前云端 Factorio 实例已满,最多同时运行 2 个实例";
return info;
}
std::string safeOwner = sanitizeName(owner);
std::string safeDisplayName = sanitizeName(displayName.empty() ? owner : displayName);
long long now = static_cast<long long>(time(nullptr));
std::string suffix = randomSuffix();
std::string instanceId =
"cfactorio_" + safeOwner + "_" + std::to_string(now) + "_" + suffix;
instanceId = sanitizeName(instanceId);
std::string containerName =
"cloud_factorio_" + safeOwner + "_" + suffix;
containerName = sanitizeName(containerName);
std::string instanceDir =
cloudFactorioRoot() + "/instances/" + safeOwner + "/" + instanceId;
std::string saveDir = instanceDir + "/saves";
std::string configDir = instanceDir + "/config";
std::string mkdirCmd =
"mkdir -p " + shellQuote(saveDir) + " " +
shellQuote(configDir) + " " +
shellQuote(cloudFactorioRoot() + "/exports/" + safeOwner);
int mkdirExit = 0;
std::string mkdirOut = execCommand(mkdirCmd, &mkdirExit);
if (mkdirExit != 0) {
info.msg = "创建 Factorio 数据目录失败:" + mkdirOut;
return info;
}
for (int hostPort = CLOUD_FACTORIO_PORT_START;
hostPort <= CLOUD_FACTORIO_PORT_END;
++hostPort) {
if (!isUdpPortAvailable(hostPort)) {
continue;
}
std::ostringstream cmd;
cmd << "docker rm -f " << containerName << " >/dev/null 2>&1; "
<< "timeout " << DOCKER_CMD_TIMEOUT_SECONDS << "s "
<< "docker run -d --pull=never "
<< "--restart=no "
<< "--memory=1536m "
<< "--cpus=1.50 "
<< "--label p2p.cloud_game=1 "
<< "--label p2p.game_id=factorio "
<< "--label p2p.owner=" << safeOwner << " "
<< "--label p2p.display_name=" << safeDisplayName << " "
<< "--label p2p.instance_id=" << instanceId << " "
<< "--name " << containerName << " "
<< "-p " << hostPort << ":" << CLOUD_FACTORIO_CONTAINER_PORT << "/udp "
<< "-v " << shellQuote(instanceDir) << ":/factorio "
<< CLOUD_FACTORIO_IMAGE;
std::cout << "☁️ [CloudFactorio] 准备启动云端 Factorio 实例\n";
std::cout << "☁️ [CloudFactorio] owner=" << owner
<< " instance=" << instanceId
<< " port=" << hostPort << "\n";
std::cout << "☁️ [CloudFactorio] command=" << cmd.str() << "\n";
int exitCode = 0;
std::string output = execCommand(cmd.str(), &exitCode);
std::cout << "☁️ [CloudFactorio] docker exitCode=" << exitCode
<< " output=" << output << "\n";
if (exitCode != 0) {
std::string cleanupCmd =
"timeout " + std::to_string(DOCKER_CMD_TIMEOUT_SECONDS) +
"s docker rm -f " + containerName + " >/dev/null 2>&1";
int cleanupExit = 0;
execCommand(cleanupCmd, &cleanupExit);
continue;
}
std::string containerId = extractContainerId(output);
if (containerId.empty()) {
std::string cleanupMsg;
stopInstance(containerName, cleanupMsg);
continue;
}
if (!isContainerRunning(containerId)) {
std::string cleanupMsg;
stopInstance(containerId, cleanupMsg);
std::cerr << "❌ [CloudFactorio] 容器启动后未保持 running,已清理。container="
<< containerId << " cleanup=" << cleanupMsg << "\n";
continue;
}
info.success = true;
info.instanceId = instanceId;
info.containerId = containerId;
info.containerName = containerName;
info.owner = safeOwner;
info.displayName = safeDisplayName;
info.host = resolveGameHost();
info.port = hostPort;
info.saveDir = saveDir;
info.configDir = configDir;
info.msg = "云端 Factorio 服务器已启动";
std::cout << "✅ [CloudFactorio] 已启动实例"
<< " instance=" << info.instanceId
<< " container=" << info.containerId
<< " host=" << info.host
<< " port=" << info.port << "\n";
return info;
}
info.success = false;
info.msg = "34197-34198 没有可用 UDP 端口,或 Docker 启动均失败";
return info;
}
原型bool GameInstanceManager::stopCloudFactorioInstance(const std::string& owner, const std::string& instanceId, std::string& outMsg)
参数
| 参数 | 说明 |
|---|
owner | 实例所有者 |
instanceId | 实例标识 |
outMsg | 输出状态说明 |
执行流程
- 查找实例
- 校验所有权
- 执行 docker rm -f
- 更新实例状态
- 返回结果
关联接口
GameInstanceManager::exportCloudFactorioSaveGameInstanceManager::listCloudFactorioInstances
查看完整实现
game_instance_manager.cpp
bool GameInstanceManager::stopCloudFactorioInstance(const std::string& owner,
const std::string& instanceId,
std::string& outMsg)
{
if (owner.empty() || instanceId.empty()) {
outMsg = "owner 或 instanceId 为空";
return false;
}
std::string safeOwner = sanitizeName(owner);
std::string safeInstance = sanitizeName(instanceId);
std::string findCmd =
"timeout " + std::to_string(DOCKER_CMD_TIMEOUT_SECONDS) +
"s docker ps -aq "
"--filter label=p2p.cloud_game=1 "
"--filter label=p2p.game_id=factorio "
"--filter label=p2p.owner=" + safeOwner + " "
"--filter label=p2p.instance_id=" + safeInstance;
int findExit = 0;
std::string containerId = trim(execCommand(findCmd, &findExit));
if (findExit != 0) {
outMsg = "查询云端 Factorio 容器失败";
return false;
}
if (containerId.empty()) {
outMsg = "未找到对应云端 Factorio 实例,可能已经停止";
return true;
}
return stopInstance(containerId, outMsg);
}