java解析魔兽争霸3录像_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
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;
// 解析Action
analysisAction(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 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 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 对 所有人 说:
结束语:
《Java解析魔兽争霸3录像W3G文件》系列博文就写到这里了,当然还有很多可以继续写的东西,例如判断玩家胜负,判断玩家的英雄、单位等。需要源码的同学可以在评论中留下E-mail。
java解析魔兽争霸3录像_Java解析魔兽争霸3录像W3G文件(五):Action和APM计算相关推荐
- Java解析魔兽争霸3录像W3G文件(五):Action和APM计算
在游戏进行中,玩家会进行各种操作,例如编队.移动.技能.造建筑等,这些操作就是Action.APM(Actions Per Minute),表示每分钟的操作次数,APM可以很好的反映玩家的手速和实力, ...
- java 接收前台富文本_java 解析富文本处理 img 标签
很多项目都需要到富文本来添加内容,就好比新闻啊,旅游景点之类的,都需要使用富文本去添加数据,然而怎么我这边就发现了两个问题 1)怎样将富文本的图片的 src 获取出来? 2)后台上传的时候用的是相对路 ...
- java解析xml文档_Java解析xml文件
读xml文件: xml文件内容: Java Eclipse Swift Xcode C# Visual Studio 代码: package XMLParse; import java.io.File ...
- java中domain什么意思_java解析URL中domain、端口和协议的两种方法
java解析URL中domain.端口和协议的两种方法 Java代码 收藏代码 @Test public void parseDomain() throws IOException { for (in ...
- java 证书缺乏扩展项_java解析证书的例子(包括基本项目、扩展项目)
package ciso.security.test; /** * Title: Light Weight APIs for crypto * Description: 一个上海CA证书(根证书和用户 ...
- java解析soap返回报文_java解析soap响应报文
本文从 SOAP 报文的编写.在 C#语言中 定义远程响应端的参数设置,以发送报文请求.解析接收的 SOAP 报文从而提 取所需的业务数据等方面,对该系统的设计和实现...... java通过报文交换 ...
- java解析excel的方法_Java解析Excel内容的方法
本文实例讲述了Java解析Excel内容的方法.分享给大家供大家参考.具体实现方法如下: import java.io.File; import java.io.FileInputStream; im ...
- java解析excel的工具_Java 解析 Excel 工具 easyexcel
软件介绍 easyexcel -- JAVA 解析 Excel 工具 Java 解析.生成 Excel 比较有名的框架有 Apache poi.jxl .但他们都存在一个严重的问题就是非常的耗内存,p ...
- java 解析xml字符串的_java 解析xml字符串
在做第三方接口测试的时候很容遇到接口返回的数据类型是xml串.把我解决问题的方法记录下来,供参考. 需要引入dom4j的jar包: package com.test; import java.util ...
- java文字转pdf格式_java根据富文本生成pdf文件
public classPdfUtil {/** 生成pdf工具类 * wmy 12:40 2019/8/9 * @Param [guideBook, pdfPath] * @return java. ...
最新文章
- 1行代码搞定Latex公式编写,这个4.6M的Python小插件,堪称论文必备神器
- 当钢铁直男去应聘...... | 每日趣闻
- 支持回调处理 php函数,PHP支持回调的函数有哪些
- 解决执行go get时报错的问题:dial tcp: lookup xxx.com on 8.8.8.8:53: no such host
- MySQL数据库创建及删除操作
- python用tsne降维_哈工大硕士实现了 11 种经典数据降维算法,源代码库已开放
- ASP.NET Core 2.2 : 十六.扒一扒2.2版更新的新路由方案
- 创建用户故事地图的步骤
- 面向Transformer模型的高效预训练方法
- 亚泰盛世携NB物理实验邀你莅临第66届中国教育装备展
- 计算机技术在中医药的应用,计算机技术在中医药领域的应用概况
- (生活篇)对恋爱谈心大事件的思考与反思——于五周年纪念日20211225
- 节后 威金/Viking 来拜年
- java 实现pdf转换成图片
- 不懂异或?一文详解移位操作符,位操作符
- 联想第二季度业绩创纪录 所有业务实现强劲增长
- Python中的逻辑运算符号
- read函数和fread函数的区别
- 【矩阵求导】对于复向量l1-norm 1范数的求导
- 基于jquery+php+mysql 制作 仿google日历记事
热门文章
- Mac 下的代码比对工具
- 基于MSP430f5529的红外循迹小车
- freeswitch被外国IP攻击盗打的防护措施
- FineReport(一)帆软报表的安装
- 东芝2000ac废粉盒怎么二次利用_阜新降级组件回收厂家,废太阳能板回收_振昌_光伏...
- 有限元微分方程求解方法,能量原理,瑞利里兹法,伽辽金法(曾攀有限元分析)
- 使用ffmpeg推流rtmp
- 【Python】numpy库和scipy库的安装与使用
- 计算机初级培训 ppt,《计算机初级培训》PPT课件
- 介质天线的设计原理_以水为媒介的介质天线的制作方法