Java解析魔兽争霸3录像W3G文件(五):Action和APM计算
在游戏进行中,玩家会进行各种操作,例如编队、移动、技能、造建筑等,这些操作就是Action。APM(Actions Per Minute),表示每分钟的操作次数,APM可以很好的反映玩家的手速和实力,当然也有高APM的菜鸟和低APM的高手。
在魔兽录像文件中,需要记录下玩家的操作,这些操作是记录在游戏时间段(TimeSlot)数据块中的,这在上一篇博文中有提到。
结构:
在TimeSlot中从第6字节开始到数据块结尾的部分,包含多个玩家数据块(CommandData Block):
1字节:玩家ID;
2~3字节:数据块剩余字节数n;
4~n+4字节:包含该玩家对应的多个操作数据块(ActionBlock)。
ActionBlock结构:
1字节:ActionID,表示操作类型,例如暂停游戏操作的ActionID是0x01;
剩余字节:Action参数,该部分结构需要根据ActionID来确定,有些Action没有这部分。
由于Action类型很多,每种ActionID对应的ActionBlock结构这里不一一列出,下面列出一小部分:
1.暂停游戏
ActionID:0x01
字节数:1
计算APM:否
2.继续游戏
ActionID:0x02
字节数:1
计算APM:否
3.编队
ActionID:0x17
字节数:4+n*8
计算APM:是
结构:
1字节:ActionID;
2字节:队伍编号(0~9);
3~4字节:选择单位的数量n;
5~4+n*8:选择单位
4.选择编队
ActionID:0x18
字节数:3
计算APM:是
结构:
1字节:ActionID;
2字节:队伍编号(0~9);
3字节:未知0x03
其他Action请参考文档:http://w3g.deepnode.de/files/w3g_actions.txt
APM计算:
由于暴雪官方并没有提供APM的计算方式,所以APM计算的方式都是前辈牛人们总结出来的,不同的录像分析软件算出来的APM可能会有一些误差。
APM的值等于玩家的有效Action数量除以玩家游戏时间的分钟数。
一个ActionBlock一般表示玩家的一次操作,例如一次编队、暂停游戏。其中部分操作要算入APM中,例如编队,而有些操作不计算APM,例如暂停游戏。另外,还有的ActionBlock是自动生成的,也不算入APM。Action是否算入APM可以查看文档w3g_actions.txt。
其中比较特殊的有ActionID为0x16的Action。这个Action表示选择或取消选择。ActionBlock的第二个字节为0x01表示选择,0x02表示取消选择。一般来说这个Action是算入APM的,但是如果两个相邻的ActionID为0x16的ActionBlock,前一个为取消选择,后一个为选择,那么这两个ActionBlock只算一次有效的Action,因为前一个是自动生成的。
在游戏进行过程中,可能会有玩家暂停游戏的情况,也有玩家在游戏结束前提前退出游戏的情况。在计算APM的时候一定要去掉这部分的时间,这样算出来的APM才准确。
下面的截图就是RepKing录像分析软件没有考虑游戏暂停导致的问题,导致玩家游戏时间大于录像的时长,APM计算不准确。
Java解析Action和APM:
在Player.java中加入action表示玩家的有效操作数:
/**
* 操作次数
*/
private int action;public int getAction() {return action;
}public void setAction(int action){this.action = action;
}
在ReplayData.java中,加入对Action的解析,在ReplayData中加入isPause表示游戏是否暂停,在游戏暂停时游戏时间不能增加:
/**
* 是否暂停
*/
private boolean isPause;/**
* 解析一个时间块
*/
private void analysisTimeSlot() {offset++;int bytes = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);offset += 2;// 游戏时间在非暂停状态下增加int timeIncrement = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);if(!isPause) {time += timeIncrement;}offset += 2;// 解析ActionanalysisAction(offset + bytes - 2);
}/*** 解析TimeSlot中的Action* @param end TimeSlot的结束位置*/
private void analysisAction(int timeSlotEnd) {while(offset != timeSlotEnd) {byte playerId = uncompressedDataBytes[offset];Player player = getPlayById(playerId);int action = player.getAction();offset++;int commandDataBlockbytes = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);offset += 2;int commandDataBlockEnd = offset + commandDataBlockbytes;boolean lastActionWasDeselect = false;while(offset != commandDataBlockEnd) {byte actionId = uncompressedDataBytes[offset];boolean thisActionIsDeselect = false;if(actionId == 0x16 && uncompressedDataBytes[offset + 1] == 0x02) {thisActionIsDeselect = true;}switch (actionId) {// 暂停游戏case 0x01:isPause = true;offset++;break;// 继续游戏case 0x02:isPause = false;offset++;break;case 0x03:offset += 2;break;case 0x04:case 0x05:offset++;break;case 0x06:offset++;while(uncompressedDataBytes[offset] != 0) {offset++;}offset++;break;case 0x07:offset += 5;break;case 0x10:offset += 15;action++;break;case 0x11:offset += 23;action++;break;case 0x12:offset += 31;action++;break;case 0x13:offset += 39;action++;break;case 0x14:offset += 44;action++;break;case 0x16:offset++;byte selectMode = uncompressedDataBytes[offset];offset++;if(selectMode == 0x02) {action++;} else {if(!lastActionWasDeselect) {action++;}}int number = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);offset += 2;offset += number * 8;break;case 0X17:offset += 2;int n = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);offset += 2;offset += n * 8;action++;break;case 0x18:offset += 3;action++;break;case 0x19:offset += 13;break;case 0x1a:offset++;break;case 0x1b:offset += 10;break;case 0x1c:offset += 10;action++;break;case 0x1d:offset += 9;action++;break;case 0x1e:offset += 6;action++;break;case 0x21:offset += 9;break;case 0x20:case 0x22:case 0x23:case 0x24:case 0x25:case 0x26:case 0x29:case 0x2a:case 0x2b:case 0x2c:case 0x2f:case 0x30:case 0x31:case 0x32:offset++;break;case 0x27:case 0x28:case 0x2d:offset += 6;break;case 0x2e:offset += 5;break;case 0x50:offset += 6;break;case 0x51:offset += 10;break;case 0x60:offset += 9;while(uncompressedDataBytes[offset] != 0) {offset++;}offset++;break;case 0x61:offset++;action++;break;case 0x62:offset += 13;break;case 0x66:case 0x67:offset++;action++;break;case 0x68:offset += 13;break;case 0x69:case 0x6a:offset += 17;break;case 0x75:offset += 2;break;}lastActionWasDeselect = thisActionIsDeselect;}player.setAction(action);}
}
在Test.java中,输出计算得到的玩家APM值:
package com.xxg.w3gparser;import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.zip.DataFormatException;public class Test {public static void main(String[] args) throws IOException, W3GException, DataFormatException {Replay replay = new Replay(new File("C:/Documents and Settings/Administrator/桌面/131224_[UD]crabby_VS_[ORC]LuciferLVZ_LostTemple_RN.w3g"));Header header = replay.getHeader();System.out.println("版本:1." + header.getVersionNumber() + "." + header.getBuildNumber());long duration = header.getDuration();System.out.println("时长:" + convertMillisecondToString(duration));UncompressedData uncompressedData = replay.getUncompressedData();System.out.println("游戏名称:" + uncompressedData.getGameName());System.out.println("游戏创建者:" + uncompressedData.getCreaterName());System.out.println("游戏地图:" + uncompressedData.getMap());List<Player> list = uncompressedData.getPlayerList();for(Player player : list) {System.out.println("---玩家" + player.getPlayerId() + "---");System.out.println("玩家名称:" + player.getPlayerName());if(player.isHost()) {System.out.println("是否主机:主机");} else {System.out.println("是否主机:否");}System.out.println("游戏时间:" + convertMillisecondToString(player.getPlayTime()));System.out.println("操作次数:" + player.getAction());System.out.println("APM:" + player.getAction() * 60000 / player.getPlayTime());if(player.getTeamNumber() != 12) {System.out.println("玩家队伍:" + (player.getTeamNumber() + 1));switch(player.getRace()) {case 0x01:case 0x41:System.out.println("玩家种族:人族");break;case 0x02:case 0x42:System.out.println("玩家种族:兽族");break;case 0x04:case 0x44:System.out.println("玩家种族:暗夜精灵");break;case 0x08:case 0x48:System.out.println("玩家种族:不死族");break;case 0x20:case 0x60:System.out.println("玩家种族:随机");break;}switch(player.getColor()) {case 0:System.out.println("玩家颜色:红");break;case 1:System.out.println("玩家颜色:蓝");break;case 2:System.out.println("玩家颜色:青");break;case 3:System.out.println("玩家颜色:紫");break;case 4:System.out.println("玩家颜色:黄");break;case 5:System.out.println("玩家颜色:橘");break;case 6:System.out.println("玩家颜色:绿");break;case 7:System.out.println("玩家颜色:粉");break;case 8:System.out.println("玩家颜色:灰");break;case 9:System.out.println("玩家颜色:浅蓝");break;case 10:System.out.println("玩家颜色:深绿");break;case 11:System.out.println("玩家颜色:棕");break;}System.out.println("障碍(血量):" + player.getHandicap() + "%");if(player.isComputer()) {System.out.println("是否电脑玩家:电脑玩家");switch (player.getAiStrength()){case 0:System.out.println("电脑难度:简单的");break;case 1:System.out.println("电脑难度:中等难度的");break;case 2:System.out.println("电脑难度:令人发狂的");break;}} else {System.out.println("是否电脑玩家:否");}} else {System.out.println("玩家队伍:裁判或观看者");}}List<ChatMessage> chatList = uncompressedData.getReplayData().getChatList();for(ChatMessage chatMessage : chatList) {String chatString = "[" + convertMillisecondToString(chatMessage.getTime()) + "]";chatString += chatMessage.getFrom().getPlayerName() + " 对 ";switch ((int)chatMessage.getMode()) {case 0:chatString += "所有人";break;case 1:chatString += "队伍";break;case 2:chatString += "裁判或观看者";break;default:chatString += chatMessage.getTo().getPlayerName();}chatString += " 说:" + chatMessage.getMessage();System.out.println(chatString);}}private static String convertMillisecondToString(long millisecond) {long second = (millisecond / 1000) % 60;long minite = (millisecond / 1000) / 60;if (second < 10) {return minite + ":0" + second;} else {return minite + ":" + second;}}}
输出:
版本:1.26.6059
时长:15:39
游戏名称:当地局域网内的游戏 (96
游戏创建者:962030958
游戏地图:Maps\E-WCLMAP\(2)AncientIsles.w3x
---玩家1---
玩家名称:962030958
是否主机:主机
游戏时间:15:38
操作次数:2635
APM:168
玩家队伍:1
玩家种族:不死族
玩家颜色:黄
障碍(血量):100%
是否电脑玩家:否
---玩家2---
玩家名称:flygogogo
是否主机:否
游戏时间:15:37
操作次数:3483
APM:222
玩家队伍:2
玩家种族:兽族
玩家颜色:蓝
障碍(血量):100%
是否电脑玩家:否
[0:12]962030958 对 所有人 说:glgl
[4:53]962030958 对 所有人 说:==
[4:53]962030958 对 所有人 说:猫咪爬到键盘上了
[4:53]962030958 对 所有人 说:g?
[4:53]flygogogo 对 所有人 说:g
[15:32]flygogogo 对 所有人 说:
[15:35]flygogogo 对 所有人 说:gg
[15:36]flygogogo 对 所有人 说:
参考文档:http://w3g.deepnode.de/files/w3g_actions.txt
结束语:
《Java解析魔兽争霸3录像W3G文件》系列博文就写到这里了,当然还有很多可以继续写的东西,例如判断玩家胜负,判断玩家的英雄、单位等。需要源码的同学可以在评论中留下E-mail。
Github地址:https://github.com/wucao/jw3gparser 欢迎star!
作者:叉叉哥 转载请注明出处:http://blog.csdn.net/xiao__gui/article/details/19326555
Java解析魔兽争霸3录像W3G文件(五):Action和APM计算相关推荐
- Java解析魔兽争霸3录像W3G文件(三):解析游戏开始前的信息
上一篇博文中,通过对压缩数据块的解压缩以及合并,得到了解压缩的字节数组.从现在开始,就要处理这个数据. 这个部分的数据主要包括两大类信息:一类是游戏开始前的信息,例如游戏地图,游戏玩家,队伍.种族情况 ...
- java 解析m3u8的实例_m3u8文件完整实例及TS流抓取
参考文档: http://blog.csdn.net/blueboyhi/article/details/40107683 如下 是一个华数影视的M3U8文件链接 http://chyd-sn.was ...
- 用Java解析:您可以使用的所有工具和库
如果需要从Java解析语言或文档,则从根本上讲有三种方法可以解决问题: 使用支持该特定语言的现有库:例如用于解析XML的库 手动构建自己的自定义解析器 生成解析器的工具或库:例如ANTLR,可用于构建 ...
- java读取war3模型_GitHub - wucao/jw3gparser: Java Warcraft Ⅲ Replay Parser(Java解析《魔兽争霸3》游戏录像工具)...
jw3gparser Java解析<魔兽争霸3>游戏录像工具,可解析w3g.nwg(网易对战平台录像)格式录像. 使用方法 public class Test { public stati ...
- java解析dxf文件_浅析JVM方法解析、创建和链接
一:前言 上周末写了一篇文章<你知道Java类是如何被加载的吗?>,分析了HotSpot是如何加载Java类的,干脆趁热打铁,本周末再来分析下Hotspot又是如何解析.创建和链接类方法的 ...
- java解析Excel文件
下文介绍java解析Excel文件的方案 前置准备 1.第三方jar包或者Maven配置 org.apache.poi的jar包 Maven配置如下 <groupId>org.apache ...
- XML解析 (JAVA解析xml文件)java+Dom4j+Xpath xml文件解析根据子节点得到父节点 查找校验xml文件中相同的节点属性值 java遍历文件夹解析XML
XML解析 (JAVA解析xml文件)java+Dom4j+Xpath xml文件解析根据子节点得到父节点 以及查找xml文件中相同的节点属性值 项目背景:这是本人实习中所碰到的项目,当时感觉很棘手, ...
- IDEA Java解析GeoJson.json文件
IDEA Java解析GeoJson.json文件 一.遇到的问题 1. 无法导入成功 2. org.geotools.StyleFactory is not an ImageIO SPI class ...
- java 解析 csv 文件
文章分类:JavaEye 一.貌似有bug,不行用 二.或 三. 的方法 Java代码 import java.io.BufferedReader; import java.io.FileInpu ...
最新文章
- Python 进程之间共享数据(全局变量)
- linux系统的交换分区怎么分配?
- 阿里云助力重庆打造“亚洲最智能大型城市”
- 怎么把丢失的计算机放回桌面,不小心把电脑桌面开始哪里放在右边了,怎么把它放回原处啊...
- Java中的异步等待
- 简单的高可用集群实验
- java synchronized概念用法
- MATLAB画柱状图对比
- 大数据掀人类文明革命 探索更多未知
- 启动BPM的5个步骤
- 网站加载图片慢 网页响应慢 网页优化
- mmdetection报错 TypeError: vars() argument must have __dict__ attribute
- (附源码)ssm介绍信智能实现系统 毕业设计 260930
- php设计模式:单例模式
- 姑苏寻古[小刚执笔]
- VL817以及迭代型号VL817S原理图规格书示例
- safari开发模式联调h5,网页检查器空白
- 1-Click PC Tuneup软件-破解实录-[下]
- 2019中兴校招面经整理
- mysql查询面试_mysql查询面试一