骑马与砍杀中文站论坛

 找回密码
 注册(Register!)

QQ登录

只需一步,快速开始

搜索
购买CDKEY 衣谷三国
查看: 3917|回复: 13

[原创] WSE2联网扩展系列7-打造专属玩家账户系统|实操

[复制链接]

25

主题

229

回帖

151

积分

见习骑士

Rank: 3

UID
3181770
第纳尔
1911
精华
0
互助
15
荣誉
0
贡献
10
魅力
170
注册时间
2020-5-5
鲜花(45) 鸡蛋(0)
发表于 2025-6-8 19:14:24 | 显示全部楼层 |阅读模式
本帖最后由 zz010606 于 2025-6-8 19:27 编辑

WSE2联网扩展系列7-实操教程:
实现玩家登录功能的模组开发指南
在本期教程中,我们将深入讲解如何利用WSE2联网扩展为你的模组添加玩家账户功能。从基础配置到代码实现,手把手教你完成身份验证系统,确保玩家可以安全登录并保存个人数据。
账户系统的作用:
  • 1 让玩家的进度、装备、金币等需要联网保存的数据不会随着存档丢失,可以长期保存,避免每次开新存档就重置。
  • 2 个性化体验:让每个玩家拥有独特的游戏身份,支持扩展更多功能。
  • 3 多人联机整合:结合WSE2的联网功能,实现玩家间的数据交互,如公会系统、排行榜或交易市场。【排行榜和交易市场我已经都代码实现,后续会逐步开源】
  • 4 安全性与反作弊:通过追踪账户信息,可以防止通过单机的作弊功能作弊,确保联机环境的公平性。

无论你是想为模组添加完整的联网内容,还是仅仅需要一个基础的账户系统,这篇实操指南都将提供清晰的步骤与实用技巧,帮助你快速实现功能!
注:本位最终实现结果为:玩家可以通过账号密码登录,并且也可以登出。若登录成功,则云端会生成一份token并返回给骑砍。

【注】本文将有大量源码+实操教程。需要MS基础知识、web基础知识、数据库基础知识、登录鉴权token的基础知识。不适合纯小白。
一、前期准备:
1给你的MS添加好WSE2且安装好相关WSE2的SDK,若不知道如何安装WSE2的SDK,可以去这个链接:(1) Warband Script Enhancer 2 (v1.1.3.6) | TaleWorlds Forums
2相关环境:python3环境、python3的flask库、flask_mysqldb库。【您若懂web,可以不需要用楼主的flask库,选择任何您觉得喜欢的库即可,如java的springboot库】
3准备云服务器一台。确保服务器的80端口可被访问到。

二、数据库设计
【如果不懂数据库,可以把我下面的内容问问 deepseek】
数据库中添加表"player",添加字段:"id" 、"account"、"password"、"token" "nikename"【这四个字段是必须要的,我的图片里还有其他字段,可以不添加】
id是唯一主键,用于确保每条玩家信息唯一.
account是玩家的账号
password是玩家的密码
token是请求令牌,您不知道这有什么用没关系,但这个登录和拓展其他所有功能必须要的功能。
088968a9-9556-4f56-bbc5-bf6fd2815cdf.png

往表里添加一条记录 id:"001"        nikename:"玩家1"        account:"user01"        password:"admin"        token:"" 【token先空着】

三、 骑砍的 MS代码设计
module_game_menu.py添加一个网络菜单:
1您可以将这个菜单添加到任何菜单底下,我是添加到camp菜单内


  1. ("camp",mnf_scale_picture,
  2.    "You set up camp. What do you want to do?",
  3.    "none",
  4.    [
  5.      (assign, "$g_player_icon_state", pis_normal),
  6.      (set_background_mesh, "mesh_pic_camp"),
  7.     ],
  8.     [
  9.       ("camp_action_1",[(eq,"$cheat_mode",1)],"{!}Cheat: Walk around.",
  10.        [(set_jump_mission,"mt_ai_training"),
  11.         (call_script, "script_setup_random_scene"),
  12.         (change_screen_mission),
  13.         ]
  14.        ),
  15.        #添加这些内容-开始# # #
  16.      ("camp_network_menu",[], "Network Content",
  17.        [
  18.          (jump_to_menu, "mnu_camp_network_menu"),
  19.      ]
  20.        ),
  21.        #添加这些内容-结束
复制代码

结果展示:
1749378323304.jpg

2现在开始写这个菜单具体的代码


  1. #网络菜单开始-
  2.   ("camp_network_menu",mnf_scale_picture,
  3.    "Install WSE2 to use network functions",
  4.    "none",
  5.    [
  6.      (set_background_mesh, "mesh_pic_camp"),#设置背景图片
  7.     ],
  8.     [
  9.       ("network_login",[
  10.         (eq,"$g_network_state",0),
  11.       ],"Log in.",#登录
  12.        [
  13.            (start_presentation, "prsnt_network_login"),
  14.         ]
  15.        ),

  16.        ("network_logout",[
  17.         (eq,"$g_network_state",1),
  18.         
  19.       ],"Log out.",#登出
  20.        [
  21.            (send_post_message_to_url_advanced,"str_network_doLogout_url","@version=1.0&token=12345",s0,"script_network_logout", "script_network_fail", 0),#这里的"@version=1.0&token=12345"是随便写的,可以为任意值
  22.            (jump_to_menu,"mnu_camp"),
  23.         ]
  24.        ),
  25.       
  26.        ("network_back",[],"back.",
  27.        [
  28.            (change_screen_return),
  29.         ]
  30.        ),
  31.    

  32.       ]
  33.   ),
  34.   #网络菜单结束-
复制代码
结果展示:
a4da7d7e-70b4-4832-b076-46a1c835bbf2.png

3现在开始写具体的登录界面,请在module_presentation.py添加下面代码

  1. #网络登录界面
  2.     ("network_login",0,mesh_load_window,[
  3.       (ti_on_presentation_load,
  4.        [(set_fixed_point_multiplier, 1000),
  5.         
  6.         (str_store_string, s1, "@network login"),
  7.         (create_text_overlay, reg1, s1, tf_center_justify),
  8.         (position_set_x, pos1, 300),
  9.         (position_set_y, pos1, 500),
  10.         (overlay_set_position, reg1, pos1),
  11.         (overlay_set_text, reg1, s1),


  12.         (str_store_string, s1, "@network password"),
  13.         (create_text_overlay, reg2, s1, tf_center_justify),
  14.         (position_set_x, pos1, 300),
  15.         (position_set_y, pos1, 400),
  16.         (overlay_set_position, reg2, pos1),
  17.         (overlay_set_text, reg2, s1),

  18.         (create_simple_text_box_overlay, "$g_presentation_obj_name_login"),
  19.         (create_simple_text_box_overlay, "$g_presentation_obj_name_pwd"),
  20.         (position_set_x, pos1, 500),
  21.         (position_set_y, pos1, 500),
  22.         (position_set_x, pos2, 500),
  23.         (position_set_y, pos2, 400),
  24.         (overlay_set_position, "$g_presentation_obj_name_login", pos1),
  25.         (overlay_set_position, "$g_presentation_obj_name_pwd", pos2),

  26.         
  27.         (create_button_overlay, "$g_presentation_obj_name_network_commit", "@Continue...", tf_center_justify),
  28.         (position_set_x, pos1, 350),
  29.         (position_set_y, pos1, 200),
  30.         (overlay_set_position, "$g_presentation_obj_name_network_commit", pos1),

  31.         (create_button_overlay, "$g_presentation_obj_name_network_cancel", "@Cancel", tf_center_justify),
  32.         (position_set_x, pos1, 550),
  33.         (position_set_y, pos1, 200),
  34.         (overlay_set_position, "$g_presentation_obj_name_network_cancel", pos1),
  35.         (presentation_set_duration, 999999),
  36.         ]),
  37.       (ti_on_presentation_event_state_change,
  38.        [(store_trigger_param_1, ":object"),
  39.         (try_begin),
  40.           (eq, ":object", "$g_presentation_obj_name_login"),
  41.           (str_store_string, s7, s0),#s7存账号
  42.         (else_try),
  43.           (eq, ":object", "$g_presentation_obj_name_pwd"),
  44.           (str_store_string, s8, s0),#s8存密码
  45.         (else_try),
  46.           (eq, ":object", "$g_presentation_obj_name_network_cancel"),
  47.           (presentation_set_duration, 0),
  48.         (else_try),
  49.           (eq, ":object", "$g_presentation_obj_name_network_commit"),

  50.           (str_store_string,s0,"str_network_account_and_pwd"),
  51.           (send_post_message_to_url_advanced,"str_network_doLogin_url","@user_agent_string",s0,"script_network_login", "script_network_fail", 0),         
  52.           (presentation_set_duration, 0),
  53.         
  54.         (try_end),
  55.         ]),
  56.       ]),
  57.       #网络登录结束
复制代码
结果:
735e33ee-6d63-4e58-b1f6-727d3ec681a8.png


4补充全str_network_doLogin_url与str_network_account_and_pwd
请打开module_string.py 添加下面代码: 请注意,请把"http://www.xxxxx.com/doLogin"这部分换成你自己的云服的公网ip如 "http://127.0.0.1/doLogin"
  1. ("network_show_info","Network Infomation"),
  2. ("network_doLogin_url","http://www.xxxx.com/doLogin"),
  3. ("network_doLogout_url","http://www.xxxx.com/doLogout"),
  4. ("network_account_and_pwd","account={s7}&password={s8}"),
复制代码


5补充上面所有提到的script

请打开module_script.py 添加下面代码:



  1. #接口失败回调
  2. ("network_fail", [
  3.       (dialog_box,"@网 络 错 误 ", "str_network_show_info"),
  4.       (jump_to_menu,"mnu_camp"),
  5.     ]),
  6. #接口成功回调
  7.      ("network_login", [#
  8.     # (store_script_param, ":num_integers", 1),
  9.     # (store_script_param, ":num_strings", 2),
  10.     (try_begin),
  11.       (eq,reg0,1),
  12.       (assign,"$g_network_state",1),
  13.       (troop_set_plural_name, "trp_token", s0),#在骑砍存储token值,之后所有的请求就都会带着这个值发给服务器了
  14.       (dialog_box,"@登 陆 成  功 ", "str_network_show_info"),#如果接受回来的数据是1就表示成功
  15.       (jump_to_menu,"mnu_camp"),
  16.       (display_message, s0, 0x66ccff),
  17.     (else_try),
  18.       (eq,reg0,2),
  19.       (dialog_box,"@账 号 密 码 错 误 ", "str_network_show_info"),#2表示账号密码错误
  20.     (else_try),
  21.       (eq,reg0,3),#3表示状态有问题
  22.       (dialog_box,"@用 户 状 态 不 可 用 ", "str_network_show_info"),
  23.     (try_end),
  24.     ]),

  25.     #登出
  26.      ("network_logout", [
  27.     # (store_script_param, ":num_integers", 1),
  28.     # (store_script_param, ":num_strings", 2),
  29.     (assign,"$g_network_state",0),
  30.     (dialog_box,"@注 销 成  功 ", "str_network_show_info"),


  31.     ]),
复制代码

6请打开module_troop.py 添加下面代码:

  1. #token
  2.     ["token","token","token",tf_female|tf_guarantee_boots|tf_guarantee_armor,0,0,fac_commoners,[],def_attrib|level(2),wp(40),knows_common,woman_face_1,woman_face_2],
  3.     #版本号
  4.     ["version","version","109",tf_female|tf_guarantee_boots|tf_guarantee_armor,0,0,fac_commoners,[],def_attrib|level(2),wp(40),knows_common,woman_face_1,woman_face_2],
复制代码

四、服务器开发

楼主是采用flask项目架构,分为很多工具py文件和业务文件,解耦程度高。不方便展示。所以楼主把核心内容的“登录”,“登出”功能提炼到一份文件出来:
服务器需要配置python3环境,安装好flask库与flask_mysqldb 库。然后运行本文件即可。
服务器开发部分核心就是 登录功能接收到账号密码后进行检验是否对应合理,然后针对结果返回'1"或者“2” ”3“ 。登出核心功能就是根据传输过来的token清楚云端的对应的token。
【但是楼主给的骑砍MS 代码里logout接口其实是废的,没有把token传输过来,这是因为楼主用于实现登出主要是利用MS里$g_network_state这个全局变量,若登出就变为0,这样玩家就进入不到网络接口的菜单里了,就得强制登录了哈哈

  1. from datetime import datetime
  2. import uuid
  3. from flask import Flask, request
  4. from flask_mysqldb import MySQL
  5. from urllib.parse import parse_qs

  6. app = Flask(__name__)

  7. # MySQL配置
  8. app.config['MYSQL_HOST'] = 'localhost'  # 根据实际情况修改
  9. app.config['MYSQL_USER'] = 'root'       # 根据实际情况修改
  10. app.config['MYSQL_PASSWORD'] = ' '       # 根据实际情况修改
  11. app.config['MYSQL_DB'] = 'your_db'      # 根据实际情况修改
  12. mysql = MySQL(app)

  13. # 工具函数 - 获取成功状态码
  14. def getSuccessStateCode():
  15.     return "1"

  16. # 工具函数 - 检查UserAgent
  17. def checkUserAgent(userAgent):
  18.     cursor = mysql.connection.cursor()
  19.     userAgent = parse_qs(userAgent)

  20.     version = userAgent.get('version', [''])[0]  # 获取version
  21.     token = userAgent.get('token', [''])[0]      # 获取token

  22.     # 验证Token有效性
  23.     cursor.execute("SELECT id FROM player WHERE token = %s", (token,))
  24.     user = cursor.fetchone()
  25.     if not user:
  26.         return '4', token  # Token无效

  27.     # 检查版本是否匹配
  28.     cursor.execute("SELECT version FROM mod_update_logs ORDER BY update_date DESC LIMIT 1")
  29.     latest_version = cursor.fetchone()

  30.     if latest_version:
  31.         latest_version = latest_version[0]
  32.         if version != latest_version:
  33.             cursor.close()
  34.             return '6', token  # 版本不匹配

  35.     return "ok", token

  36. # 工具函数 - 检查UserAgent状态
  37. def checkUserAgentStatus(status):
  38.     if status == "4":
  39.         return status
  40.     return None

  41. # 登录接口
  42. @app.route('/doLogin', methods=['POST'])
  43. def doLogin():
  44.     cursor = mysql.connection.cursor()
  45.    
  46.     # 获取登录数据
  47.     data = request.form
  48.     account = data.get('account')
  49.     pwd = data.get('password')

  50.     # 查询用户信息
  51.     cursor.execute("SELECT account, password, state FROM player WHERE account = %s", (account,))
  52.     user = cursor.fetchone()

  53.     # 验证用户
  54.     if not user:  # 用户不存在
  55.         return '2'
  56.     if user[1] != pwd:  # 密码错误
  57.         return '2'
  58.     if user[2] != 1:  # 账号状态异常
  59.         return '3'

  60.     # 生成唯一Token
  61.     while True:
  62.         token = str(uuid.uuid4())
  63.         cursor.execute("SELECT * FROM player WHERE token = %s", (token,))
  64.         if not cursor.fetchone():
  65.             break

  66.     # 更新用户Token和登录时间
  67.     cursor.execute(
  68.         "UPDATE player SET token = %s, last_login_time = %s WHERE account = %s",
  69.         (token, datetime.now(), account)
  70.     )
  71.     cursor.connection.commit()
  72.     cursor.close()
  73.    
  74.     # 返回成功响应和Token
  75.     return f"{getSuccessStateCode()}|{token}"

  76. # 登出接口
  77. @app.route('/doLogout', methods=['POST'])
  78. def doLogout():
  79.     cursor = mysql.connection.cursor()

  80.     # 检查UserAgent
  81.     userAgent = request.headers.get('User-Agent')
  82.     userAgentStatus, token = checkUserAgent(userAgent)
  83.     if checkUserAgentStatus(userAgentStatus):
  84.         return checkUserAgentStatus(userAgentStatus)

  85.     try:
  86.         # 清空用户Token
  87.         cursor.execute("UPDATE player SET token = NULL WHERE token = %s", (token,))
  88.         mysql.connection.commit()
  89.         return '1'  # 登出成功

  90.     except Exception as e:
  91.         print(f"登出错误: {e}")
  92.         return '0'  # 登出失败

  93.     finally:
  94.         cursor.close()

  95. if __name__ == '__main__':
  96.     app.run(host='0.0.0.0',debug=True)
复制代码



实践我帖子时候遇到问题,帖子底下留言。或者加我企鹅号1730239726
写帖不易,路过看帖子欢迎您写个留言或者给个花,激励楼主继续更新


评分

参与人数 1魅力 +1 收起 理由
英勇的苹果 + 1 我看不懂,但我大受震撼!

查看全部评分

鲜花鸡蛋

815208129  在2025-8-11 19:00  送朵鲜花  并说:我非常同意你的观点,送朵鲜花鼓励一下
815208129  在2025-8-11 19:00  送朵鲜花  并说:我非常同意你的观点,送朵鲜花鼓励一下
英勇的苹果  在2025-6-9 10:36  送朵鲜花  并说:我看不懂,但我大受震撼!
huagao  在2025-6-8 23:34  送朵鲜花  并说:我非常同意你的观点,送朵鲜花鼓励一下

1

主题

5

回帖

7

积分

平民

Rank: 1

UID
3657494
第纳尔
17
精华
0
互助
1
荣誉
0
贡献
0
魅力
0
注册时间
2024-5-22
鲜花(5) 鸡蛋(0)
发表于 2025-6-8 20:09:01 | 显示全部楼层
非常具有开创性的工作

34

主题

242

回帖

198

积分

见习骑士

Rank: 3

UID
2462463
第纳尔
1607
精华
0
互助
23
荣誉
0
贡献
0
魅力
85
注册时间
2015-3-3
鲜花(62) 鸡蛋(0)
发表于 2025-6-8 20:43:24 | 显示全部楼层
好高深。。太强了!!!!

25

主题

229

回帖

151

积分

见习骑士

Rank: 3

UID
3181770
第纳尔
1911
精华
0
互助
15
荣誉
0
贡献
10
魅力
170
注册时间
2020-5-5
鲜花(45) 鸡蛋(0)
 楼主| 发表于 2025-6-8 20:53:18 | 显示全部楼层
WKSPT 发表于 2025-6-8 20:09
非常具有开创性的工作

感谢支持                     

25

主题

229

回帖

151

积分

见习骑士

Rank: 3

UID
3181770
第纳尔
1911
精华
0
互助
15
荣誉
0
贡献
10
魅力
170
注册时间
2020-5-5
鲜花(45) 鸡蛋(0)
 楼主| 发表于 2025-6-8 20:56:54 | 显示全部楼层
战争傀儡阿格兰 发表于 2025-6-8 20:43
好高深。。太强了!!!!

感谢阿格兰支持~

19

主题

1452

回帖

511

积分

皇家侍卫长[官方战队队长]

光辉骑士团[UTD]
战团ID:UTD_awe23

Rank: 6Rank: 6

UID
2102975
第纳尔
7295
精华
0
互助
10
荣誉
2
贡献
0
魅力
73
注册时间
2014-8-1

2024国庆青训杯季军勋章第二届梦幻联赛奉献勋章第十一届战团中国联赛征战勋章第十一届战团中国联赛铁骨勋章第一届梦幻联赛参与勋章第十届战团中国联赛征战勋章战团正版勋章骑士美德之英勇勋章[杰出会员活跃勋章]元老骑士勋章霸主正版勋章

鲜花(77) 鸡蛋(2)
发表于 2025-6-8 21:34:33 | 显示全部楼层
往数据库里存明文密码是大忌

25

主题

229

回帖

151

积分

见习骑士

Rank: 3

UID
3181770
第纳尔
1911
精华
0
互助
15
荣誉
0
贡献
10
魅力
170
注册时间
2020-5-5
鲜花(45) 鸡蛋(0)
 楼主| 发表于 2025-6-8 21:44:04 | 显示全部楼层
BattleField 发表于 2025-6-8 21:34
往数据库里存明文密码是大忌

对的,一般不明文。此贴仅示例展示,具体可详细扩展的我都没详写

16

主题

130

回帖

204

积分

见习骑士

Rank: 3

UID
3304622
第纳尔
690
精华
0
互助
26
荣誉
3
贡献
0
魅力
158
注册时间
2021-12-28
鲜花(31) 鸡蛋(0)
发表于 2025-6-9 00:23:34 | 显示全部楼层
很好的贴心,期待解决密码明文的问题

5

主题

3036

回帖

947

积分

骑士

Rank: 4Rank: 4

UID
86936
第纳尔
12688
精华
0
互助
5
荣誉
1
贡献
0
魅力
71
注册时间
2008-8-10

原版正版勋章战团正版勋章元老骑士勋章汉匈决战正版勋章维京征服正版勋章霸主正版勋章

鲜花(65) 鸡蛋(1)
发表于 2025-6-9 10:37:00 | 显示全部楼层
我看不懂,但我大受震撼!
PS:同样关心密码安全问题,别被盗号啊……

25

主题

229

回帖

151

积分

见习骑士

Rank: 3

UID
3181770
第纳尔
1911
精华
0
互助
15
荣誉
0
贡献
10
魅力
170
注册时间
2020-5-5
鲜花(45) 鸡蛋(0)
 楼主| 发表于 2025-6-9 14:38:47 | 显示全部楼层
英勇的苹果 发表于 2025-6-9 10:37
我看不懂,但我大受震撼!
PS:同样关心密码安全问题,别被盗号啊……

哈哈哈哈啊哈,这个按理说确实要优化一下。但只要没人攻击就不用特地防御。

34

主题

1002

回帖

2556

积分

子爵[版主]

Rank: 7Rank: 7Rank: 7

UID
2755938
第纳尔
6278
精华
12
互助
13
荣誉
98
贡献
85
魅力
265
注册时间
2016-7-5

骑砍中文站APP会员勋章骑士美德之忠诚勋章[杰出会员精华勋章]骑士美德之英勇勋章[杰出会员活跃勋章]骑士美德之正义勋章[杰出会员荣誉勋章]元老骑士勋章

鲜花(250) 鸡蛋(0)
发表于 2025-6-9 17:10:20 来自手机 | 显示全部楼层
加密保存就行了,这个有许多成熟方案

25

主题

229

回帖

151

积分

见习骑士

Rank: 3

UID
3181770
第纳尔
1911
精华
0
互助
15
荣誉
0
贡献
10
魅力
170
注册时间
2020-5-5
鲜花(45) 鸡蛋(0)
 楼主| 发表于 2025-6-10 14:36:21 | 显示全部楼层
偃靖 发表于 2025-6-9 17:10
加密保存就行了,这个有许多成熟方案

嗯的                           

0

主题

1

回帖

5

积分

平民

Rank: 1

UID
3782030
第纳尔
0
精华
0
互助
1
荣誉
0
贡献
0
魅力
0
注册时间
2025-7-24
鲜花(0) 鸡蛋(0)
发表于 2025-7-24 23:14:26 | 显示全部楼层
大佬,这个帖子的意思是可以进行大地图联机吗

25

主题

229

回帖

151

积分

见习骑士

Rank: 3

UID
3181770
第纳尔
1911
精华
0
互助
15
荣誉
0
贡献
10
魅力
170
注册时间
2020-5-5
鲜花(45) 鸡蛋(0)
 楼主| 发表于 2025-7-27 18:46:39 | 显示全部楼层
哇哇哇2324 发表于 2025-7-24 23:14
大佬,这个帖子的意思是可以进行大地图联机吗

不是,但快了。                                 
您需要登录后才可以回帖 登录 | 注册(Register!)

本版积分规则

Archiver|手机版|小黑屋|骑马与砍杀中文站

GMT+8, 2025-9-22 07:21 , Processed in 0.135125 second(s), 40 queries , Gzip On, MemCached On.

Powered by Discuz! X3.4 Licensed

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表