Files
zogo/cafegame/game.go
T
4566704 63d0a0caf8 feat(cafegame): 自 go-hua 迁入网咖游戏识别与运行记录框架
- 游戏来源常量/按目录特征识别(云更新/网维大师/易乐游)/RunAppRecord 上报框架
- 网吧业务专用包, 附测试、examples/cafegame 例程与包 README
2026-09-20 12:37:10 +08:00

679 lines
14 KiB
Go

//go:build windows
package cafegame
import (
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
// IsGameDirByYGX 云更新 id从40000开始
func IsGameDirByYGX(dir string) (bool, AppData, error) {
info := AppData{}
gamePatch := filepath.Join(dir, "gamepatch")
ok, err := pathExists(gamePatch)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
gconfig := filepath.Join(gamePatch, "gconfig.ini")
ok, err = pathExists(gconfig)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
id, _ := getIdByNumberIco(dir)
if id < 40000 {
return false, info, nil
}
info.AppId = id
info.AppName = filepath.Base(dir)
info.AppDir = dir
info.AppSource = SourceYGX
return true, info, nil
}
// IsGameDirByStory 蘑菇 id从8000开始
func IsGameDirByStory(dir string) (bool, AppData, error) {
info := AppData{}
gamePatch := filepath.Join(dir, "gamepatch")
ok, err := pathExists(gamePatch)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
gconfig := filepath.Join(gamePatch, "gconfig.ini")
ok, err = pathExists(gconfig)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
id, _ := getIdByNumberIco(dir)
if id < 10000 {
return false, info, nil
}
info.AppId = id
info.AppName = filepath.Base(dir)
info.AppDir = dir
info.AppSource = SourceStory
return true, info, nil
}
// IsGameDirByWWDS 网维大师 ID 从 10001 开始
func IsGameDirByWWDS(dir string) (bool, AppData, error) {
info := AppData{}
SunwardPkg := filepath.Join(dir, "SunwardPkg.idx")
ok, err := pathExists(SunwardPkg)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
SunwardPkgCks := filepath.Join(dir, "SunwardPkg.cks")
ok, err = pathExists(SunwardPkgCks)
if err != nil {
return false, info, err
}
if !ok {
fmt.Printf("没有cks文件 dir:%s\n", dir)
return false, info, nil
}
// 不能有这文件 这是盖伦文件
GameIndex := filepath.Join(dir, "GameIndex.txt")
ok, err = pathExists(GameIndex)
if err != nil {
return false, info, err
}
if ok {
return false, info, nil
}
// 不能有这文件,这是易乐游文件
GameInfodb := filepath.Join(dir, "GameInfodb.gz")
ok, err = pathExists(GameInfodb)
if err != nil {
return false, info, err
}
if ok {
return false, info, nil
}
// 不能有这文件 这是好司机文件
hsjGame := filepath.Join(dir, "hsjGame.exe")
ok, err = pathExists(hsjGame)
if err != nil {
return false, info, err
}
if ok {
return false, info, nil
}
id, _ := getIdBySunwardPkg(dir)
if id < 10000 {
return false, info, nil
}
info.AppId = id
info.AppName = filepath.Base(dir)
info.AppDir = dir
info.AppSource = SourceWWDS
return true, info, nil
}
// IsGameDirByGoodGame 盖伦 id应该是从 1001开始 steam是第一个游戏 1011
func IsGameDirByGoodGame(dir string) (bool, AppData, error) {
info := AppData{}
GameIndex := filepath.Join(dir, "GameIndex.txt")
ok, err := pathExists(GameIndex)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
GameCksum := filepath.Join(dir, "GameCksum.txt")
ok, err = pathExists(GameCksum)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
id, _ := getIdBySunwardPkg(dir)
if id < 1000 {
return false, info, nil
}
info.AppId = id
info.AppName = filepath.Base(dir)
info.AppDir = dir
info.AppSource = SourceGoodGame
return true, info, nil
}
// IsGameDirByYLY 易乐游
func IsGameDirByYLY(dir string) (bool, AppData, error) {
info := AppData{}
GameInfodb := filepath.Join(dir, "GameInfodb.gz")
ok, err := pathExists(GameInfodb)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
Gamever := filepath.Join(dir, "Gamever.ver")
ok, err = pathExists(Gamever)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
// TODO: 没有游戏ID,只有GUID
guid, _ := GetGuidByYLY(dir)
if guid == "{00000000-0000-0000-0000-000000000000}" {
return false, info, nil
}
info.AppId = 0
info.AppName = filepath.Base(dir)
info.AppDir = dir
info.AppSource = SourceYLY
info.AppGuid = guid
return true, info, nil
}
// IsGameDirByHaoSiJi 好司机
func IsGameDirByHaoSiJi(dir string) (bool, AppData, error) {
info := AppData{}
hsjGame := filepath.Join(dir, "hsjGame.exe")
ok, err := pathExists(hsjGame)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
hsjLoader := filepath.Join(dir, "hsjLoader.exe")
ok, err = pathExists(hsjLoader)
if !ok {
return false, info, nil
}
if err != nil {
return false, info, err
}
SunwardPkg := filepath.Join(dir, "SunwardPkg.idx")
ok, err = pathExists(SunwardPkg)
if !ok {
return false, info, nil
}
if err != nil {
return false, info, err
}
id, _ := getIdBySunwardPkg(dir)
if id == 0 {
return false, info, nil
}
guid, _ := GetGuidByYLY(dir)
if guid == "{00000000-0000-0000-0000-000000000000}" {
return false, info, nil
}
info.AppId = id
info.AppName = filepath.Base(dir)
info.AppDir = dir
info.AppSource = SourceHaoSiJi
info.AppGuid = guid
return true, info, nil
}
// IsGameDirByMTGX 马蹄更新 id从10000开始
func IsGameDirByMTGX(dir string) (bool, AppData, error) {
info := AppData{}
patchdata := filepath.Join(dir, "patchdata")
ok, err := pathExists(patchdata)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
config := filepath.Join(patchdata, "config.ini")
ok, err = pathExists(config)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
ndstorrent := filepath.Join(dir, "ndstorrent.idx")
ok, err = pathExists(ndstorrent)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
id, _ := getIdByNumberIco(dir)
if id < 10000 {
return false, info, nil
}
info.AppId = id
info.AppName = filepath.Base(dir)
info.AppDir = dir
info.AppSource = SourceMTGX
return true, info, nil
}
// IsGameDirByFast 火箭 id从8000开始
func IsGameDirByFast(dir string) (bool, AppData, error) {
info := AppData{}
patchdata := filepath.Join(dir, "patchdata")
ok, err := pathExists(patchdata)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
rkttorrent := filepath.Join(dir, "rkttorrent.idx")
ok, err = pathExists(rkttorrent)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
gconfig := filepath.Join(patchdata, "config.ini")
ok, err = pathExists(gconfig)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
id, _ := getIdByNumberIco(dir)
if id < 8000 {
return false, info, nil
}
info.AppId = id
info.AppName = filepath.Base(dir)
info.AppDir = dir
info.AppSource = SourceFast
return true, info, nil
}
// IsGameDirByWfGame WF游戏
func IsGameDirByWfGame(dir string) (bool, AppData, error) {
info := AppData{}
ndsgameres := filepath.Join(dir, "ndsgameres.index")
ok, err := pathExists(ndsgameres)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
dd_launcher := filepath.Join(dir, "dd_launcher.json")
ok, err = pathExists(dd_launcher)
if err != nil {
return false, info, err
}
if !ok {
return false, info, nil
}
id, _ := getIdByDdLauncher(dir)
if id == 0 {
return false, info, nil
}
info.AppId = id
info.AppName = filepath.Base(dir)
info.AppDir = dir
info.AppSource = SourceWfGame
return true, info, nil
}
// 网维大师 盖伦 好司机
func getIdBySunwardPkg(dir string) (int, error) {
path := filepath.Join(dir, "SunwardPkg.idx")
file, err := os.Open(path)
if err != nil {
fmt.Println("Error opening file:", err)
return 0, err
}
defer file.Close()
// Read a 4-byte integer
var num int32
err = binary.Read(file, binary.BigEndian, &num)
if err != nil {
fmt.Println("Error reading integer:", err)
return 0, err
}
// fmt.Println("Read integer:", num)
return int(num), nil
}
// 云更新 蘑菇 火箭使用
func getIdByNumberIco(dir string) (int, error) {
// 获该目录下所有文件
fileList, _ := getFileList(dir)
for _, s := range fileList {
ext := filepath.Ext(s)
// 判断是否ico图标
if strings.ToLower(ext) == ".ico" {
// fmt.Printf("ico: %s\n", s)
// 通过ico图标的数字确定游戏ID
fileName := strings.TrimSuffix(filepath.Base(s), filepath.Ext(s))
if IsAllNumberHandle(fileName) {
gameId, _ := strconv.Atoi(fileName)
if gameId >= 8000 {
return gameId, nil
}
}
} else {
continue
}
}
return 0, nil
}
// DdLauncherJson DDL Launcher 平台游戏信息(JSON 配置)
type DdLauncherJson struct {
Gameid int `json:"gameid"`
}
// getIdByDdLauncher 从 DDL Launcher 配置文件中读取游戏 ID
func getIdByDdLauncher(dir string) (int, error) {
path := filepath.Join(dir, "dd_launcher.json")
ok, err := pathExists(path)
if err != nil {
return 0, err
}
if !ok {
return 0, errors.New("文件不存在")
}
buf, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
return 0, err
}
data := DdLauncherJson{}
err = json.Unmarshal(buf, &data)
if err != nil {
fmt.Println(err)
return 0, err
}
if data.Gameid == 0 {
return 0, errors.New("游戏ID为空")
}
return data.Gameid, nil
}
// GetGuidByYLY 从易乐游目录配置中读取游戏 GUID
func GetGuidByYLY(dir string) (string, error) {
path := filepath.Join(dir, "$ResMetadata$", "ResAppData.dat")
ok, err := pathExists(path)
if err != nil {
return "", err
}
if !ok {
return "", errors.New("文件不存在")
}
file, err := os.Open(path)
if err != nil {
fmt.Println(err)
return "", err
}
defer file.Close()
// 设置读取起始位置为第25个字节
_, err = file.Seek(24, 0)
if err != nil {
fmt.Println(err)
return "", err
}
// 创建一个字节切片来存储GUID
buf := make([]byte, 16)
// 读取GUID到字节切片中
_, err = file.Read(buf)
if err != nil {
fmt.Println(err)
return "", err
}
guidInt32 := binary.LittleEndian.Uint32(buf[:4])
guidInt16_1 := binary.LittleEndian.Uint16(buf[4:6])
guidInt16_2 := binary.LittleEndian.Uint16(buf[6:8])
guidBytes_1 := buf[8:10]
guidBytes_2 := buf[10:]
guid := fmt.Sprintf("{%08x-%04x-%04x-%s-%s}", guidInt32, guidInt16_1, guidInt16_2, hex.EncodeToString(guidBytes_1), hex.EncodeToString(guidBytes_2))
guid = strings.ToTitle(guid)
return guid, err
}
// GetGameInfoByDir 获取游戏信息 从目录获取
func GetGameInfoByDir(root string) (AppData, error) {
// fmt.Println(root)
data := AppData{}
// 云更新
ok, data, err := IsGameDirByYGX(root)
if ok {
return data, err
}
// 网维大师
ok, data, err = IsGameDirByWWDS(root)
if ok {
return data, err
}
// 易乐游
ok, data, err = IsGameDirByYLY(root)
if ok {
return data, err
}
// 马蹄更新
ok, data, err = IsGameDirByMTGX(root)
if ok {
return data, err
}
// 蘑菇
ok, data, err = IsGameDirByStory(root)
if ok {
return data, err
}
// 盖伦
ok, data, err = IsGameDirByGoodGame(root)
if ok {
return data, err
}
// 好司机
ok, data, err = IsGameDirByHaoSiJi(root)
if ok {
return data, err
}
// 火箭
ok, data, err = IsGameDirByFast(root)
if ok {
return data, err
}
// WF游戏
ok, data, err = IsGameDirByWfGame(root)
if ok {
return data, err
}
return data, nil
}
// GetGameInfoByPath 获取游戏信息 从路径获取
func GetGameInfoByPath(exePath string) (AppData, error) {
path := filepath.ToSlash(exePath)
pathArr := strings.Split(path, "/")
// 3个/ 比如 D:/网络游戏/英雄联盟/xxx.exe
// 长度3 是 D:/英雄联盟/xxx.exe
if len(pathArr) < 3 {
return AppData{}, errors.New("不支持1级目录")
}
dir := ""
// 连接二级目录
for i, s := range pathArr {
// 最后是文件名,不检测
if i >= len(pathArr)-1 {
break
}
// 最多支持3级目录 如 D:/网络游戏/腾讯游戏/英雄联盟/xxx.exe
if i > 4 {
break
}
if i == 0 {
dir = s
} else {
dir = filepath.Join(dir, string(filepath.Separator), s)
}
// 获取游戏信息
info, err := GetGameInfoByDir(dir)
if err != nil {
return info, err
}
if info.AppId > 0 || info.AppGuid != "" {
return info, nil
}
}
return AppData{}, nil
}
// 获取文件列表
func getFileList(dir string) ([]string, error) {
list := make([]string, 0)
//fmt.Println(dir)
f, err := os.Open(dir)
if err != nil {
fmt.Println(err)
}
files, err := f.Readdir(-1)
f.Close()
if err != nil {
fmt.Println(err)
return nil, err
}
for _, file := range files {
if !file.IsDir() && file.Name() != "$RECYCLE.BIN" && file.Name() != "System Volume Information" {
list = append(list, filepath.Join(dir, file.Name()))
}
}
return list, nil
}
// IsAllNumberHandle /** 是否全部是数字 */
func IsAllNumberHandle(str string) bool {
// 空串不认为是数字
if str == "" {
return false
}
anyReplaceList := []int32{'.', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}
for strIndex, strInt := range str {
if strIndex == 0 {
if strInt == '-' {
continue
}
}
if IsHaveReplaceList(strInt, anyReplaceList) {
continue
}
return false
}
return true
}
// IsHaveReplaceList 是否在过滤列表中
func IsHaveReplaceList(strInt int32, replaceList []int32) bool {
for _, chStrInt := range replaceList {
if chStrInt == strInt {
return true
}
}
return false
}
// pathExists 判断一个文件或文件夹是否存在
// 输入文件路径,根据返回的bool值来判断文件或文件夹是否存在
func pathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}