Commit a7073477 authored by nanahira's avatar nanahira

Merge branch 'master' into ai-play

parents 65a995d8 fd2bf6ea
Pipeline #41576 passed with stages
in 10 minutes and 11 seconds
......@@ -10,7 +10,7 @@ RUN apt update && \
# windbot
RUN git clone --depth=1 https://code.mycard.moe/nanahira/windbot /tmp/windbot && \
cd /tmp/windbot && \
xbuild /property:Configuration=Release /property:TargetFrameworkVersion="v4.0" && \
xbuild /property:Configuration=Release /property:TargetFrameworkVersion="v4.5" && \
mv /tmp/windbot/bin/Release /ygopro-server/windbot && \
cp -rf /ygopro-server/ygopro/cards.cdb /ygopro-server/windbot/ && \
rm -rf /tmp/windbot
......
......@@ -38,7 +38,7 @@ RUN git clone --branch=server --recursive --depth=1 https://code.mycard.moe/nana
strip ygopro && \
mkdir replay expansions && \
rm -rf .git* bin obj build ocgcore cmake lua premake* sound textures .travis.yml *.txt appveyor.yml LICENSE README.md *.lua strings.conf system.conf && \
ls gframe | sed '/game.cpp/d' | xargs -I {} rm -rf gframe/{}
ls gframe | sed '/config.h/d' | xargs -I {} rm -rf gframe/{}
# infos
WORKDIR /ygopro-server
......
......@@ -27,6 +27,7 @@
"stop": false,
"full": "服务器已爆满",
"max_rooms_count": 0,
"max_mem_percentage": 95,
"side_timeout": false,
"replay_delay": true,
"hide_name": false,
......@@ -40,6 +41,7 @@
"expansions_path": [
"./expansions"
],
"extra_script_path": [],
"i18n": {
"auto_pick": false,
"default": "zh-cn",
......@@ -79,7 +81,8 @@
"get": "https://sapi.moecube.com:444/biu-tips/tips.json",
"get_zh": false,
"interval": 30000,
"interval_ingame": 120000
"interval_ingame": 120000,
"prefix": "Tip: "
},
"dialogues": {
"enabled": true,
......@@ -118,7 +121,8 @@
"TMR": true
},
"ready_time": 20,
"hang_timeout": 90
"hang_timeout": 90,
"extra_modes": {}
},
"mysql": {
"enabled": false,
......@@ -245,8 +249,9 @@
"endpoint": "https://api.openai.com",
"token": "sk-xxxx",
"model": "gpt-4o-mini",
"system_prompt": "",
"comment": "{{player}} and {{opponent}} will be replaced in the prompt",
"system_prompt": "你是{{windbot}},一名与{{player}}实时互动的游戏对手。你的回复应简短、有趣、贴合当前情境,增强玩家沉浸感。避免冗长解释或重复内容,保持自然流畅,并且每次回复不能超过100个字。",
"comment": "{{player}} and {{windbot}} will be replaced in the prompt",
"token_limit": 12000,
"extra_opts": {}
},
"test_mode": {
......
......@@ -411,8 +411,8 @@
"bad_user_name": "请输入正确的用户名",
"server_full": "服务器已经爆满,请稍候再试",
"too_much_connection": "同时开启的客户端数量过多 ",
"banned_ip_login": "您的账号已被封禁",
"banned_user_login": "您的账号已被封禁",
"banned_ip_login": "您的账号已被封禁。如果您没有进行违规操作且用的是流量网络,可能过几小时就好。是IP撞了。",
"banned_user_login": "您的账号已被封禁。如果您没有进行违规操作且用的是流量网络,可能过几小时就好。是IP撞了。",
"bad_name_level3": "您的用户名存在不适当的内容",
"bad_name_level2": "您的用户名存在不适当的内容",
"bad_name_level1": "您的用户名存在不适当的内容,请注意更改",
......
......@@ -16,11 +16,13 @@
"deepmerge": "^4.2.2",
"formidable": "^1.2.6",
"geoip-country-lite": "^1.0.0",
"gpt-tokenizer": "^3.0.1",
"ip6addr": "^0.2.5",
"jszip": "^3.5.0",
"load-json-file": "^6.2.0",
"lzma": "^2.3.2",
"moment": "^2.29.1",
"mustache": "^4.2.0",
"mysql": "^2.18.1",
"node-os-utils": "^1.3.2",
"p-queue": "^6.6.2",
......@@ -34,7 +36,7 @@
"underscore": "^1.11.0",
"underscore.string": "^3.3.6",
"ws": "^8.9.0",
"ygopro-deck-encode": "^1.0.9"
"ygopro-deck-encode": "^1.0.14"
},
"devDependencies": {
"@types/bunyan": "^1.8.8",
......@@ -1357,6 +1359,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gpt-tokenizer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.0.1.tgz",
"integrity": "sha512-5jdaspBq/w4sWw322SvQj1Fku+CN4OAfYZeeEg8U7CWtxBz+zkxZ3h0YOHD43ee+nZYZ5Ud70HRN0ANcdIj4qg==",
"license": "MIT"
},
"node_modules/graceful-fs": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.1.14.tgz",
......@@ -1933,6 +1941,15 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"license": "MIT",
"bin": {
"mustache": "bin/mustache"
}
},
"node_modules/mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
......@@ -3687,9 +3704,9 @@
}
},
"node_modules/ygopro-deck-encode": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/ygopro-deck-encode/-/ygopro-deck-encode-1.0.9.tgz",
"integrity": "sha512-2aw/Lr8Sg4cPXKgq71Zk/GQPTZy5GhmviptVHWqMGEW0E2qTaxwpGmsQAN2Q4OWaK1lP+3g3bZt9BaqmWYZQSw==",
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/ygopro-deck-encode/-/ygopro-deck-encode-1.0.14.tgz",
"integrity": "sha512-Q64f8U+okLBDKHw02eRYsdDMpALhYa55k0BhFqZ5k4ntRpPKFNvM9sNEbBlg2bmyi6LCf3rlEmISmLtlx9uDeA==",
"license": "MIT"
}
},
......@@ -4678,6 +4695,11 @@
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"gpt-tokenizer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.0.1.tgz",
"integrity": "sha512-5jdaspBq/w4sWw322SvQj1Fku+CN4OAfYZeeEg8U7CWtxBz+zkxZ3h0YOHD43ee+nZYZ5Ud70HRN0ANcdIj4qg=="
},
"graceful-fs": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.1.14.tgz",
......@@ -5111,6 +5133,11 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
......@@ -6446,9 +6473,9 @@
}
},
"ygopro-deck-encode": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/ygopro-deck-encode/-/ygopro-deck-encode-1.0.9.tgz",
"integrity": "sha512-2aw/Lr8Sg4cPXKgq71Zk/GQPTZy5GhmviptVHWqMGEW0E2qTaxwpGmsQAN2Q4OWaK1lP+3g3bZt9BaqmWYZQSw=="
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/ygopro-deck-encode/-/ygopro-deck-encode-1.0.14.tgz",
"integrity": "sha512-Q64f8U+okLBDKHw02eRYsdDMpALhYa55k0BhFqZ5k4ntRpPKFNvM9sNEbBlg2bmyi6LCf3rlEmISmLtlx9uDeA=="
}
}
}
......@@ -18,11 +18,13 @@
"deepmerge": "^4.2.2",
"formidable": "^1.2.6",
"geoip-country-lite": "^1.0.0",
"gpt-tokenizer": "^3.0.1",
"ip6addr": "^0.2.5",
"jszip": "^3.5.0",
"load-json-file": "^6.2.0",
"lzma": "^2.3.2",
"moment": "^2.29.1",
"mustache": "^4.2.0",
"mysql": "^2.18.1",
"node-os-utils": "^1.3.2",
"p-queue": "^6.6.2",
......@@ -36,7 +38,7 @@
"underscore": "^1.11.0",
"underscore.string": "^3.3.6",
"ws": "^8.9.0",
"ygopro-deck-encode": "^1.0.9"
"ygopro-deck-encode": "^1.0.14"
},
"license": "AGPL-3.0",
"scripts": {
......
......@@ -25,6 +25,8 @@ qs = require "querystring"
zlib = require 'zlib'
axios = require 'axios'
osu = require 'node-os-utils'
mustache = require 'mustache'
gpt_tokenizer = require 'gpt-tokenizer/model/gpt-4o'
bunyan = require 'bunyan'
log = global.log = bunyan.createLogger name: "mycard"
......@@ -511,7 +513,7 @@ loadLFList = (path) ->
catch
try
log.info("Reading YGOPro version.")
cppversion = parseInt((await fs.promises.readFile(path.resolve(settings.modules.ygopro_path, 'gframe', 'game.cpp'), 'utf8')).match(/PRO_VERSION = ([x\dABCDEF]+)/)[1], '16')
cppversion = parseInt((await fs.promises.readFile(path.resolve(settings.modules.ygopro_path, 'gframe', 'config.h'), 'utf8')).match(/PRO_VERSION = ([x\dABCDEF]+)/)[1], '16')
await setting_change(settings, "version", cppversion)
log.info "ygopro version 0x"+settings.version.toString(16), "(from source code)"
catch
......@@ -852,11 +854,11 @@ ROOM_find_or_create_by_name = global.ROOM_find_or_create_by_name = (name, player
uname=name.toUpperCase()
if settings.modules.windbot.enabled and (uname[0...2] == 'AI' or (!settings.modules.random_duel.enabled and uname == ''))
return ROOM_find_or_create_ai(name)
if settings.modules.random_duel.enabled and (uname == '' or uname == 'S' or uname == 'M' or uname == 'T' or uname == 'TOR' or uname == 'TR' or uname == 'OOR' or uname == 'OR' or uname == 'TOMR' or uname == 'TMR' or uname == 'OOMR' or uname == 'OMR' or uname == 'CR' or uname == 'CMR')
if settings.modules.random_duel.enabled and (uname == '' or uname == 'S' or uname == 'M' or uname == 'T' or uname == 'TOR' or uname == 'TR' or uname == 'OOR' or uname == 'OR' or uname == 'TOMR' or uname == 'TMR' or uname == 'OOMR' or uname == 'OMR' or uname == 'CR' or uname == 'CMR' or settings.modules.random_duel.extra_modes[uname] != undefined)
return await ROOM_find_or_create_random(uname, player_ip)
if room = ROOM_find_by_name(name)
return room
else if memory_usage >= 95 or (settings.modules.max_rooms_count and rooms_count >= settings.modules.max_rooms_count)
else if memory_usage >= settings.modules.max_mem_percentage or (settings.modules.max_rooms_count and rooms_count >= settings.modules.max_rooms_count)
return null
else
room = new Room(name)
......@@ -910,8 +912,9 @@ ROOM_find_or_create_random = global.ROOM_find_or_create_random = (type, player_i
else
return null
if result.random_type=='S' then result.welcome2 = '${random_duel_enter_room_single}'
if result.random_type=='M' then result.welcome2 = '${random_duel_enter_room_match}'
if result.random_type=='T' then result.welcome2 = '${random_duel_enter_room_tag}'
else if result.random_type=='M' then result.welcome2 = '${random_duel_enter_room_match}'
else if result.random_type=='T' then result.welcome2 = '${random_duel_enter_room_tag}'
else result.welcome2 = settings.modules.random_duel.extra_modes[type]?.welcome ? ''
return result
ROOM_find_or_create_ai = global.ROOM_find_or_create_ai = (name)->
......@@ -1626,6 +1629,11 @@ class Room
path.resolve(settings.modules.ygopro_path, s)
)
.join(',')
YGOPRO_EXTRA_SCRIPT: settings.modules.extra_script_path
.map((s) ->
path.resolve(settings.modules.ygopro_path, s)
)
.join(',')
}
}
)
......@@ -1722,6 +1730,7 @@ class Room
form_data.append 'start', @start_time
form_data.append 'end', end_time
form_data.append 'arena', @arena
form_data.append 'nonce', Math.random().toString()
post_score_process = () ->
axios.post settings.modules.arena_mode.post_score, form_data,
......@@ -3369,7 +3378,7 @@ ygopro.stoc_send_random_tip = (client)->
if settings.modules.tips.split_zh and tips.tips_zh.length and client.lang == "zh-cn"
tip_type = "tips_zh"
if settings.modules.tips.enabled && tips.tips.length && !client.is_local && !client.closed
ygopro.stoc_send_chat(client, "Tip: " + tips[tip_type][Math.floor(Math.random() * tips[tip_type].length)])
ygopro.stoc_send_chat(client, settings.modules.tips.prefix + tips[tip_type][Math.floor(Math.random() * tips[tip_type].length)])
await return
ygopro.stoc_send_random_tip_to_room = (room)->
if settings.modules.tips.enabled && tips.tips.length
......@@ -3708,14 +3717,30 @@ ygopro.ctos_follow 'CHAT', true, (buffer, info, client, server, datas)->
cancel = true
if not cancel and settings.modules.chatgpt.enabled and room.windbot and not client.is_post_watcher and client.pos < 2 and not client.is_local
# session_key = "#{settings.modules.chatgpt.session}:#{settings.port}:#{CLIENT_get_authorize_key(client)}"
if room.is_requesting_chatgpt
return false
room.is_requesting_chatgpt = true
if not room.chatgpt_conversation
room.chatgpt_conversation = []
openai_req_body = {
messages: [
{ role: "user", content: msg }
],
messages: Array.from(room.chatgpt_conversation),
model: settings.modules.chatgpt.model
}
openai_req_body.messages.push { role: "user", content: msg }
shrink_index = 0
if settings.modules.chatgpt.system_prompt
openai_req_body.messages.unshift { role: "system", content: settings.modules.chatgpt.system_prompt.replace }
openai_req_body.messages.unshift { role: "system", content: mustache.render(settings.modules.chatgpt.system_prompt, {
player: client.name,
windbot: room.windbot.name,
}, undefined, { escape: (v) -> v }) }
shrink_index = 1
# trim conversation if too long
shrink_count = 0
while !gpt_tokenizer.isWithinTokenLimit(openai_req_body.messages, settings.modules.chatgpt.max_tokens)
if openai_req_body.messages.length <= (1 + shrink_index)
break
openai_req_body.messages.splice(shrink_index, 2) # remove the oldest user+assistant pair
shrink_count += 2
Object.assign(openai_req_body, settings.modules.chatgpt.extra_opts)
axios.post("#{settings.modules.chatgpt.endpoint}/v1/chat/completions", openai_req_body, {
timeout: 300000,
......@@ -3732,8 +3757,15 @@ ygopro.ctos_follow 'CHAT', true, (buffer, info, client, server, datas)->
ygopro.stoc_send_chat_to_room(room, chunk.join(''), 1 - client.pos)
else
ygopro.stoc_send_chat_to_room(room, ' ', 1 - client.pos)
# save text
if shrink_count > 0
room.chatgpt_conversation.splice(0, shrink_count)
room.chatgpt_conversation.push { role: "user", content: msg }
room.chatgpt_conversation.push { role: "assistant", content: text }
).catch((err) ->
log.error "CHATGPT ERROR", err
).finally(() ->
room.is_requesting_chatgpt = false
)
return false
if !(room and (room.random_type or room.arena)) and not settings.modules.mycard.enabled
......@@ -3899,14 +3931,10 @@ ygopro.ctos_follow 'UPDATE_DECK', true, (buffer, info, client, server, datas)->
else
log.warn("GET ATHLETIC FAIL", client.name, athleticCheckResult.message)
if settings.modules.tournament_mode.enabled and settings.modules.tournament_mode.deck_check
client_deck_obj = YGOProDeck.fromUpdateDeckPayload(buffer)
if settings.modules.challonge.enabled and client.challonge_info and client.challonge_info.deckbuf
trim_deckbuf = (buf) ->
mainc = buf.readUInt32LE(0)
sidec = buf.readUInt32LE(4)
# take first (2 + mainc + sidec) * 4 bytes
return buf.slice(0, (2 + mainc + sidec) * 4)
deckbuf_from_challonge = Buffer.from(client.challonge_info.deckbuf, "base64")
if trim_deckbuf(deckbuf_from_challonge).equals(trim_deckbuf(buffer))
deck_obj = YGOProDeck.fromUpdateDeckPayload(Buffer.from(client.challonge_info.deckbuf, "base64"))
if deck_obj.isEqual(client_deck_obj, { ignoreOrder: true })
#log.info("deck ok: " + client.name)
return deck_ok("${deck_correct_part1} #{client.challonge_info.name} ${deck_correct_part2}")
else
......@@ -3915,16 +3943,14 @@ ygopro.ctos_follow 'UPDATE_DECK', true, (buffer, info, client, server, datas)->
else
decks = await fs.promises.readdir(settings.modules.tournament_mode.deck_path)
if decks.length
found_deck=false
for deck in decks
if deck_name_match(deck, client.name)
found_deck=deck
found_deck = decks.find((deck) -> deck_name_match(deck, client.name))
if found_deck
deck_text = await fs.promises.readFile(settings.modules.tournament_mode.deck_path+found_deck,{encoding:"ASCII"})
deck_obj = YGOProDeck.fromYdkString(deck_text)
deck_main=deck_obj.main.concat(deck_obj.extra)
deck_side=deck_obj.side
if _.isEqual(buff_main, deck_main) and _.isEqual(buff_side, deck_side)
# put extra cards to main
deck_obj.main = deck_obj.main.concat(deck_obj.extra)
deck_obj.extra = []
if client_deck_obj.isEqual(deck_obj, { ignoreOrder: true })
#log.info("deck ok: " + client.name)
return deck_ok("${deck_correct_part1} #{found_deck} ${deck_correct_part2}")
else
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment