设为首页收藏本站|繁體中文

Project1

 找回密码
 注册会员
搜索
查看: 123|回复: 4
打印 上一主题 下一主题

[原创发布] 简单热更新系统(rmxp)

[复制链接]

Lv3.寻梦者

梦石
0
星屑
2611
在线时间
171 小时
注册时间
2020-8-9
帖子
113
跳转到指定楼层
1
发表于 5 天前 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有帐号?注册会员

x
本帖最后由 939034448 于 2025-4-14 12:04 编辑

一个简单的热更新系统,没有做文件校验,实现原理是在Gitee创建一个仓库,通过读取仓库简介中的信息获得版本号和需要更新的文件清单,然后把需要被下载的文件挂到仓库里。
确定云端版本号已经迭代后会根据提前写在仓库简介里的文件列表查找需要下载的文件,下载完成后会逐个解压,缺点是不适用于加密过的工程文件,并且被下载的文件默认解压在
游戏根目录,需要保证解压出的文件目录结构和原游戏目录结构相同,才能达到热更新效果;考虑到可能会对游戏内各项数据进行重写等操作,更新后的数据重载方法没有写出来,
如果只是更新数据并且希望不重启游戏热更新可以自行根据需要写一个重载数据的方法。
简介信息示例:
版本:1.0.1 文件列表:update_part1.zip,update_part2.zip 文件大小:473858,3139291
文件大小需要手动输入,每个文件大小顺序需要和前面的文件名数据一致,文件大小可以属性里看,但是要注意填到网页上的时候数字中间不要有逗号,不然会被分割导致文件大小读取异常
下面代码:
RUBY 代码复制
  1. #==============================
  2. #  Auto Update System
  3. #  Version: XP-Final-Plus
  4. #  TIME:20250413
  5. #  by:金牛
  6. #  发布论坛:rpg.blue
  7. #  部分声明在56行
  8. #网页信息格式示例: 版本:1.0.1 文件列表:update_part1.zip,update_part2.zip 文件大小:473858,3139291
  9. #使用Gitee仓库挂载更新文件和更新信息,信息写在仓库简介里。
  10. #==============================
  11. module WebContentFetcher#网页模块
  12.   # Win32API 声明
  13.   URLDownloadToCacheFile = Win32API.new('Urlmon', 'URLDownloadToCacheFile', 'ippiii', 'i')
  14.   User32_msgbox          = Win32API.new('user32', 'MessageBoxW', 'LppL', 'L')
  15.   # 正则表达式配置
  16.   CONTENT_REGEX =/版本:\s*(\d+\.\d+\.\d+)\s+文件列表:\s*([a-zA-Z0-9_\-\.]+(?:,[a-zA-Z0-9_\-\.]+)*)\s+文件大小:\s*(\d+(?:,\d+)*)/i
  17.   # UTF8与宽字符转换
  18.   def self.utf8_to_wide(str)
  19.     str.unpack("U*").pack("S*")+ "\0\0"
  20.   end
  21.   # 获取网页内容(核心方法)
  22.   def self.fetch(url)
  23.     begin
  24.       URLDownloadToCacheFile.call(0,url,buf = "\0" * 1024,1024,0,0)
  25.       content = open(buf.sub(/\0+$/){}, 'rb'){ |f| f.read }=~ CONTENT_REGEX
  26.       v= $1
  27.       f= $2
  28.       n= $3
  29.       if content !=nil
  30.       # fcontent={"version"=>v.to_s.strip,"flist"=>f.to_s.split(','),"#{f.to_s.split(',')[0]}"=>n.to_s.split(',')[0],"#{f.to_s.split(',')[1]}"=>n.to_s.split(',')[1]}   
  31.        fcontent={"version"=>v.to_s.strip,"flist"=>f.to_s.split(',')}   
  32.        j=0
  33.        for i in fcontent["flist"]
  34.          fcontent[i]=n.to_s.split(',')[j]
  35.          j+=1
  36.        end  
  37.        return fcontent
  38.       else
  39.         show_error("内容格式不匹配", url)
  40.         nil
  41.       end
  42.     rescue => e
  43.       show_error("获取失败: #{e.message}", url)
  44.       nil
  45.     end
  46.   end
  47.  
  48.   # 错误提示
  49.   def self.show_error(msg, url)
  50.     title = utf8_to_wide(msg)
  51.     text = utf8_to_wide("请检查网页内容格式或访问:\n#{url}")
  52.     User32_msgbox.call(0, text, title, 16)
  53.   end
  54. end
  55. def get_update_msg
  56.   content = WebContentFetcher.fetch(REPO_URL)
  57.   return content#返回云端获取到的信息
  58.   #version=版本号,flist=一个存储所有需要下载文件的名字数组,“文件名”=该文件大小
  59. end  
  60. #==============================
  61. #  Auto Update System
  62. #  Version: XP-Final-Plus
  63. #  TIME:20250413
  64. #==============================
  65. $msg_update#存储云端信息
  66. REPO_URL    = 'https://gitee.com/你的用户名/你的仓库名' # 仓库主页地址
  67. UPBASE_URL  = 'https://gitee.com/你的用户名//你的仓库名/releases/download/你发布的版本名/'
  68. #TMP_ZIP     = './update.temp'    # 临时文件名 不用管
  69. #FINAL_ZIP   = './update.zip'     # 最终文件名 不用管
  70. GAME_DIR    = './'              # 游戏根目录
  71. CHUNK_SIZE  = 4096#4096              # 增大分块提升稳定性
  72. MAX_RETRIES = 3                # 最大重试次数
  73. CURRENT="1.0.0"               #设定当前版本号,当云端版本号出现迭代时进入更新流程
  74. User32_msgbox = Win32API.new('user32' , 'MessageBoxW' , 'LppL'  , 'L')
  75.  
  76. #==============================
  77. # 下载进度窗口
  78. #==============================
  79. class Scene_Download
  80.   def initialize
  81.     # 创建半透明背景
  82.     @viewport = Viewport.new(0, 0, 640, 480)
  83.     @bg = Sprite.new(@viewport)
  84.     @bg.bitmap = Bitmap.new(640, 480)
  85.     @bg.bitmap.fill_rect(0, 0, 640, 480, Color.new(0,0,0,180))
  86.  
  87.     # 进度窗口
  88.     @window = Window_Base.new(160, 180, 320, 140)
  89.     @window.contents = Bitmap.new(@window.width-32, @window.height-32)
  90.     @start_time = Time.now
  91.     refresh(0, 0, 0,"")
  92.   end
  93.  
  94.   def refresh(downloaded, total, retry_count,fn)
  95.     @window.contents.clear
  96.     # 动态处理未知大小
  97.     text = "下载#{fn}: "
  98.     if total > 0
  99.       percent = (downloaded.to_f / total * 100).round
  100.       text += "#{percent}% (#{filesize_format(downloaded)}/#{filesize_format(total)})"
  101.       bar_width = (downloaded.to_f / total * 276).to_i
  102.     else
  103.       phase = (Time.now - @start_time) * 2
  104.       bar_width = (Math.sin(phase) * 50 + 50).to_i
  105.       text += "正在连接服务器#{'.' * (3 - (Time.now.to_i % 3))}"
  106.     end
  107.  
  108.     # 重试提示
  109.     text += "\n重试次数:#{retry_count}" if retry_count > 0
  110.  
  111.     # 绘制内容
  112.     @window.contents.font.color = Color.new(255,255,255)
  113.     @window.contents.draw_text(4, 0, 292, 48, text)
  114.     @window.contents.fill_rect(20, 60, 276, 16, Color.new(100,100,100))
  115.     @window.contents.fill_rect(20, 60, bar_width, 16, Color.new(0,200,0))
  116.  
  117.     Graphics.update
  118.     Input.update
  119.   end
  120.  
  121.   #==============================
  122.   # 文件大小格式化(兼容Ruby 1.8)
  123.   #==============================
  124.   def filesize_format(bytes)
  125.     return "0 B" if bytes <= 0
  126.     units = ["B", "KB", "MB", "GB"]
  127.     exp = (Math.log(bytes) / Math.log(1024)).to_i
  128.     exp = [exp, 0].max  # 替换 clamp(0,3)
  129.     exp = [exp, 3].min  # 手动实现范围限制
  130.     "%.1f %s" % [bytes.to_f / 1024 ** exp, units[exp]]
  131.   end
  132.  
  133.   def dispose
  134.     @window.dispose
  135.     @bg.dispose
  136.     @viewport.dispose
  137.   end
  138. end
  139. #==============================
  140. # 增强版文件大小获取方法(修正字节序)
  141. #==============================
  142. def get_content_length(fname)#通过文件名获取大小
  143.   msg=$msg_update[fname]#返回云端包含的信息
  144.   #version=版本号,flist=一个存储所有需要下载文件的名字数组,“文件名”=该文件大小
  145.   return msg.to_i#返回此次下载文件的大小
  146. end
  147. #==============================
  148. #对比版本号
  149. #==============================
  150. def check_verison
  151.   $msg_update=get_update_msg#获取云端信息
  152.   #version=版本号,flist=一个存储所有需要下载文件的名字数组,“文件名”=该文件大小
  153.   nv=$msg_update["version"]#云端版本号
  154.   cv=CURRENT
  155.   nv=nv.to_s.split('.')
  156.   cv=cv.to_s.split('.')
  157.   for i in 0...nv.size
  158.     cv[i]=0 if cv[i]==nil#补丁,防止云端版本需要增加版本号后缀
  159.     if nv[i].to_i-cv[i].to_i>0#版本号迭代,需要更新
  160.       return true
  161.     end  
  162.   end  
  163.   return false
  164. end  
  165. #==============================
  166. # 下载核心
  167. #==============================
  168. def xp_download(url,save_path,fn)#文件直链,保存路径,文件名
  169.   # 清理残留文件
  170.   File.delete(save_path) rescue nil
  171.   scene = Scene_Download.new
  172.   success = false
  173.   retry_count = 0
  174.  
  175.   begin
  176.     internet_open = Win32API.new('wininet', 'InternetOpenA', 'plppl', 'l')
  177.     h_internet = internet_open.call("RGSS Player", 0, 0, 0, 0)
  178.     raise "网络初始化失败" if h_internet == 0
  179.  
  180.     File.open(save_path, 'wb') do |f|
  181.       until success || retry_count >= MAX_RETRIES
  182.         begin
  183.           scene.refresh(0, 0, retry_count,fn)
  184.  
  185.           # 建立连接
  186.          h_url = Win32API.new('wininet', 'InternetOpenUrlA', 'lpplll', 'l').call(h_internet, url, 0, 0, 0x80000000 | 0x00800000 | 0x00001000, 0)
  187.          raise "连接服务器失败" if h_url == 0
  188.          # 获取文件大小
  189.          total_size = get_content_length(fn)
  190.          dynamic_mode = total_size == 0
  191.          # 下载循环
  192.           buffer = "\0" * CHUNK_SIZE
  193.           downloaded = 0
  194.           last_update = Time.now
  195.           loop do
  196.             bytes_read = [0].pack('L')
  197.             Win32API.new('wininet', 'InternetReadFile', 'lplp', 'l').call(
  198.               h_url, buffer, CHUNK_SIZE, bytes_read)
  199.             read_size = bytes_read.unpack('L').first
  200.  
  201.             break if read_size == 0
  202.  
  203.             f.write(buffer[0, read_size])
  204.             downloaded += read_size
  205.  
  206.             # 优化刷新频率
  207.             if Time.now - last_update > 0.3
  208.               scene.refresh(downloaded, total_size, retry_count,fn)
  209.  
  210.               last_update = Time.now
  211.             end
  212.           end
  213.  
  214.           success = true
  215.         rescue => e
  216.           retry_count += 1
  217.           scene.refresh(downloaded, total_size, retry_count,fn)
  218.           sleep(2**retry_count)  # 指数退避
  219.           retry if retry_count < MAX_RETRIES
  220.         ensure
  221.           Win32API.new('wininet', 'InternetCloseHandle', 'l', 'l').call(h_url) rescue nil
  222.         end
  223.       end
  224.     end
  225.  
  226.   rescue => e
  227.    # p "错误发生在:#{e.backtrace.first}"
  228.     show_message("最终错误: #{e.message}", "系统错误")
  229.   ensure
  230.     scene.dispose if scene
  231.     Win32API.new('wininet', 'InternetCloseHandle', 'l', 'l').call(h_internet) rescue nil
  232.     # 重命名临时文件
  233.   #  File.rename(TMP_ZIP, fn) if success && File.exist?(TMP_ZIP)
  234.   end
  235.  
  236.   success
  237. end
  238.  
  239. #==============================
  240. # 安全解压方法
  241. #==============================
  242. def unzip_file(zip_path, dest_dir)
  243.  
  244.    # success = system("powershell -Command \"Expand-Archive -Path '#{zip_path}' -DestinationPath '#{dest_dir}'\"")
  245.     success = system("powershell -WindowStyle Hidden -Command \"Expand-Archive -Path '#{zip_path}' -DestinationPath '#{dest_dir}'\"")
  246.     success
  247. end
  248.  
  249. #==============================
  250. # 增强版更新流程
  251. #==============================
  252. def perform_update
  253.  
  254.   # 步骤1:同步云端版本信息,判断是否需要更新
  255.   if check_verison
  256.         # 步骤2:显示确认对话框
  257.      return unless confirm_update?#不需要确认的话注释掉就行
  258.     # 步骤3:获取更新列表并下载更新
  259.     for fn in $msg_update["flist"]#遍历更新列表
  260.       url=UPBASE_URL+fn
  261.       if xp_download(url, GAME_DIR+fn,fn)
  262.       else
  263.           show_message("#{fn}下载失败,请检查网络连接", "网络错误")
  264.       end
  265.     end  
  266.     for fn in $msg_update["flist"]
  267.       Graphics.update
  268.       # 步骤3:解压文件
  269.       begin
  270.       if unzip_file(GAME_DIR+fn, GAME_DIR)
  271.               File.delete(fn)
  272.              # show_message("#{fn}更新成功!", "更新完成")
  273.       else
  274.             show_message("#{fn}解压失败,请手动解压文件", "解压错误")
  275.       end
  276.        rescue => e
  277.           show_message("解压过程发生错误:#{e.message}", "严重错误")
  278.       end  
  279.     end  
  280.     #步骤4:更新完成,重载数据库
  281.     show_message("更新成功!部分更新游戏重启后生效!", "更新完成")
  282.     #重新加载数据库的方法
  283.     #需根据自身需要实现重载方法。
  284.   end  
  285. end
  286.  
  287. #==============================
  288. # 辅助方法
  289. #==============================
  290. def utf8_to_wide(str)
  291.    str.unpack("U*").pack("S*")+ "\0\0"
  292. end
  293.  
  294. def show_message(text, title="提示")
  295.   User32_msgbox.call(0, utf8_to_wide(text), utf8_to_wide(title), 0x40)
  296. end
  297. def confirm_update?
  298.   response = User32_msgbox.call(0,
  299.     utf8_to_wide("检测到新版本,是否立即更新?"),
  300.     utf8_to_wide("版本更新"),
  301.     0x34)  # 带取消按钮的警告图标
  302.   response == 6  # IDYES
  303. end

点评

各项配置写好之后运行这个方法就行:perform_update  发表于 5 天前
因为Gitee仓库限制,每个附件大小不能超过100m  发表于 5 天前

Lv4.逐梦者

梦石
0
星屑
12034
在线时间
21773 小时
注册时间
2010-10-24
帖子
309
2
发表于 4 天前 | 只看该作者
讲真 非常厉害的工作 由衷支持技术力楼主
顺便提供一下个人收藏的另外几个帖子,也许会对您未来的开发有所助力?
https://rpg.blue/thread-403455-1-1.html
https://rpg.blue/thread-376885-1-1.html
https://rpg.blue/thread-412154-1-1.html

点评

都是很厉害的文章啊,会努力学习的  发表于 3 天前
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

拿上你的纸笔,建造一个属于你的梦想世界,加入吧。
 注册会员
找回密码

站长信箱:[email protected]|手机版|小黑屋|无图版|Project1游戏制作

GMT+8, 2025-4-19 10:47

Powered by Discuz! X3.1

© 2001-2013 Comsenz Inc.

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