在游戏进行中,玩家会进行各种操作,例如编队、移动、技能、造建筑等,这些操作就是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计算相关推荐

  1. Java解析魔兽争霸3录像W3G文件(五):Action和APM计算

    在游戏进行中,玩家会进行各种操作,例如编队.移动.技能.造建筑等,这些操作就是Action.APM(Actions Per Minute),表示每分钟的操作次数,APM可以很好的反映玩家的手速和实力, ...

  2. java 接收前台富文本_java 解析富文本处理 img 标签

    很多项目都需要到富文本来添加内容,就好比新闻啊,旅游景点之类的,都需要使用富文本去添加数据,然而怎么我这边就发现了两个问题 1)怎样将富文本的图片的 src 获取出来? 2)后台上传的时候用的是相对路 ...

  3. java解析xml文档_Java解析xml文件

    读xml文件: xml文件内容: Java Eclipse Swift Xcode C# Visual Studio 代码: package XMLParse; import java.io.File ...

  4. java中domain什么意思_java解析URL中domain、端口和协议的两种方法

    java解析URL中domain.端口和协议的两种方法 Java代码 收藏代码 @Test public void parseDomain() throws IOException { for (in ...

  5. java 证书缺乏扩展项_java解析证书的例子(包括基本项目、扩展项目)

    package ciso.security.test; /** * Title: Light Weight APIs for crypto * Description: 一个上海CA证书(根证书和用户 ...

  6. java解析soap返回报文_java解析soap响应报文

    本文从 SOAP 报文的编写.在 C#语言中 定义远程响应端的参数设置,以发送报文请求.解析接收的 SOAP 报文从而提 取所需的业务数据等方面,对该系统的设计和实现...... java通过报文交换 ...

  7. java解析excel的方法_Java解析Excel内容的方法

    本文实例讲述了Java解析Excel内容的方法.分享给大家供大家参考.具体实现方法如下: import java.io.File; import java.io.FileInputStream; im ...

  8. java解析excel的工具_Java 解析 Excel 工具 easyexcel

    软件介绍 easyexcel -- JAVA 解析 Excel 工具 Java 解析.生成 Excel 比较有名的框架有 Apache poi.jxl .但他们都存在一个严重的问题就是非常的耗内存,p ...

  9. java 解析xml字符串的_java 解析xml字符串

    在做第三方接口测试的时候很容遇到接口返回的数据类型是xml串.把我解决问题的方法记录下来,供参考. 需要引入dom4j的jar包: package com.test; import java.util ...

  10. java文字转pdf格式_java根据富文本生成pdf文件

    public classPdfUtil {/** 生成pdf工具类 * wmy 12:40 2019/8/9 * @Param [guideBook, pdfPath] * @return java. ...

最新文章

  1. 1行代码搞定Latex公式编写,这个4.6M的Python小插件,堪称论文必备神器
  2. 当钢铁直男去应聘...... | 每日趣闻
  3. 支持回调处理 php函数,PHP支持回调的函数有哪些
  4. 解决执行go get时报错的问题:dial tcp: lookup xxx.com on 8.8.8.8:53: no such host
  5. MySQL数据库创建及删除操作
  6. python用tsne降维_哈工大硕士实现了 11 种经典数据降维算法,源代码库已开放
  7. ASP.NET Core 2.2 : 十六.扒一扒2.2版更新的新路由方案
  8. 创建用户故事地图的步骤
  9. 面向Transformer模型的高效预训练方法
  10. 亚泰盛世携NB物理实验邀你莅临第66届中国教育装备展
  11. 计算机技术在中医药的应用,计算机技术在中医药领域的应用概况
  12. (生活篇)对恋爱谈心大事件的思考与反思——于五周年纪念日20211225
  13. 节后 威金/Viking 来拜年
  14. java 实现pdf转换成图片
  15. 不懂异或?一文详解移位操作符,位操作符
  16. 联想第二季度业绩创纪录 所有业务实现强劲增长
  17. Python中的逻辑运算符号
  18. read函数和fread函数的区别
  19. 【矩阵求导】对于复向量l1-norm 1范数的求导
  20. 基于jquery+php+mysql 制作 仿google日历记事

热门文章

  1. Mac 下的代码比对工具
  2. 基于MSP430f5529的红外循迹小车
  3. freeswitch被外国IP攻击盗打的防护措施
  4. FineReport(一)帆软报表的安装
  5. 东芝2000ac废粉盒怎么二次利用_阜新降级组件回收厂家,废太阳能板回收_振昌_光伏...
  6. 有限元微分方程求解方法,能量原理,瑞利里兹法,伽辽金法(曾攀有限元分析)
  7. 使用ffmpeg推流rtmp
  8. 【Python】numpy库和scipy库的安装与使用
  9. 计算机初级培训 ppt,《计算机初级培训》PPT课件
  10. 介质天线的设计原理_以水为媒介的介质天线的制作方法