炉石传说卡组代码生成机制

炉石传说套牌代码分析

炉石传说可以共享套牌了,复制一串代码在炉石里面就可以直接导入,有了系统的支持,导入套牌就方便多了。那么这串字符是如何生成的呢? 经过一些分析和学习,记录如下。

我在HearthSim这个网站上找到了相关的分析和代码,https://hearthsim.info/docs/deckstrings/ 。这个网站提供了几个编程语言的生成代码,以C#版本的为例做分析。

代码

仓库地址: https://github.com/HearthSim/HearthDb。拖下来编译时,它会再去获取另外一个炉石数据库仓库: https://github.com/HearthSim/hsdata

有了这2个仓库后, 代码就可以正常运行了。在接触陌生的代码时,了解其功能除了读文档外,就是阅读测试代码了。
PS:里面有俩项目Build会失败,组件引用冲突错乱,无视即可。只需Build HearthDB

通过阅读测试方法TestDeckStrings,可以对这段字符有个大概了解。

[TestMethod]
public void TestDeckStrings()
{
    var deck = DeckSerializer.Deserialize(DeckString);
    Assert.AreEqual(CardIds.Collectible.Warrior.GarroshHellscream, deck.GetHero().Id);
    var cards = deck.GetCards();
    Assert.AreEqual(30, cards.Values.Sum());
    var heroicStroke = cards.FirstOrDefault(c => c.Key.Id == CardIds.Collectible.Warrior.HeroicStrike);
    Assert.IsNotNull(heroicStroke);
    Assert.AreEqual(2, heroicStroke.Value);
}

可以看到字符解析为byte数组,然后对其顺序遍历解析,具体分析见下图。

AAECAQcCrwSRvAIOHLACkQP/A44FqAXUBaQG7gbnB+8HgrACiLACub8CAA==

byte[43] {
0,                +--------->   placeholder
1,                |--------->   version always 1
2,                |--------->   FormatType
1,                |--------->   Num Heroes + always 1
7,                |--------->   HeroId
2,                +--------->   numSingleCards
175, 4,         + |
145, 188, 2,    + +-------------> single card part
14,               +---------^   numDoubleCards
28,           +
176, 2,       |
145, 3,       |
255, 3,       |
142, 5,       |
168, 5,       |
212, 5,       |
164, 6,       |  +----------->  double cards part
238, 6,       |
231, 7,       |
239, 7,       |
130, 176, 2,  |
136, 176, 2,  |
185, 191, 2,  +
0                 +----------->  multi cards (more than 2) count
}

PS: 我是用了ASCII Flow,效果看起来还不错. http://asciiflow.com/

数字的编码:Base 128 Varints

值得一提的是里面的卡牌编号是由1~3个byte推算出来的,比如175, 4是一张卡,145, 188, 2是另外一张卡,而不是每个占用4个byte。这样大幅度缩减了字符串的长度,尤其是新版本卡牌宇宙卡组:) 。这个数字是如何生成的呢?我们可以在VarInt这个类里面找到。

生成

while(value != 0)
{
    var b = value & 0x7f;
    value >>= 7;
    if(value != 0)
        b |= 0x80;
    ms.WriteByte((byte)b);
}

解析

ulong result = 0;
foreach(var b in bytes)
{
    var value = (ulong)b & 0x7f;
    result |= value << length * 7;
    if((b & 0x80) != 0x80)
        break;
}

乍一看,有点难懂,不难看出这个是128进制的表示方法,不过与我们常见的进制转换算法略有不同,这里使用了|= 0x80记录了一个最高有效位(most significant bit (msb) )来标识一个数字是否结束。这个自解释的信息,使得连续数字不需要其他分隔符,因为当我们读到没有MSB时,就知道这个数字标识到头了,下一个读到的新的数字的部分。这种表示方法,是倒序表示的,即低位在前面,随后读到高位然后加和得到结果。

Protocal buffers中使用了这种编码方式:https://developers.google.com/protocol-buffers/docs/encoding

命令行语法说明怎么读

在阅读Git帮助文档时,经常会看到长长的语法说明,看起来有点晕,于是查询了这类怎么读。

$ man man

有一部分内容可以参考:
The following conventions apply to the SYNOPSIS section and can be used as a guide in other sections.

bold text type exactly as shown.
italic text replace with appropriate argument.
[-abc] any or all arguments within [ ] are optional.
-a|-b options delimited by | cannot be used together.
argument … argument is repeatable.
[expression] … entire expression within [ ] is repeatable.

另外Git book里面的参数是<>扩起来的。

 

rethrow exception and keeps original StackTrace

Expceiton rethrow

最近在改一个项目的bug,反射调用一段代码出错,结果在Log里面看不到完整的错误. 原因是Log里面记录的是抛出的Exception,而不是内部Exception. 当我修改为抛出内部Exception时,发现记录下来Exception的StackTrace是在抛出Exception的地方,而不是引发Exception的地方, 即Exception的StackTrace被修改了.

为了重现这个问题,添加一个dll

    class Processor
    {
        public string Runner(DateTime dateTime)
        {
            if (dateTime.Year > 2058)
                return "Hell! It's about time!";

            throw new Exception("Hold on..");
        }
    }

调用代码

class PluginRunner
    {
        public void ExecuteCore()
        {
            Assembly assembly = Assembly.LoadFrom("PluginTest.dll");

            object obj = assembly.CreateInstance("PluginTest.Processor");
            MethodInfo m = obj.GetType().GetMethod("Runner");

            if (m != null)
            {
                object[] methodParams = new object[] { DateTime.Now };
                var result = m.Invoke(obj, methodParams);
            }
        }
    }
        static void Main(string[] args)
        {
            PluginRunner runner = new PluginRunner();
            try
            {
                runner.ExecuteCore();
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"exception stacktrace:" + Environment.NewLine + ex.StackTrace + Environment.NewLine);
                throw;
            }

        }

由于调用方的异常抛出,此时, 记录异常为m.Invoke的堆栈,结果如下:

exception stacktrace:
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
at Canary.PluginRunner.ExecuteCore() in xxx\Canary\PluginRunner.cs:line 23
at Canary.Program.Main(String[] args) in xxx\Canary\Program.cs:line 19

随后,尝试抛出InnerException

                try
                {
                    var result = m.Invoke(obj, methodParams);
                }
                catch (Exception ex)
                {
                    if (ex.InnerException != null)
                    {
                        // ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
                        throw ex.InnerException;
                    }
                    throw ex;
                }

得到的是Invoke调用方的异常,结果如下:

exception stacktrace:
   at Canary.PluginRunner.ExecuteCore() in xxx\Canary\PluginRunner.cs:line 33
   at Canary.Program.Main(String[] args) in xxx\Canary\Program.cs:line 19

但是在断点出查看时at PluginTest.Processor.Runner(DateTime dateTime),异常在抛出后被修改了.

解决办法:
第一种是使用.NET 4.5中引入的ExceptionDispatchInfo,如官方文档所说,这种方式会保留原始堆栈.这个class的引入应该初衷是为了配合Task调用的AggregateException.
The ExceptionDispatchInfo object stores the stack trace information and Watson information that the exception contains at the point where it is captured. The exception can be thrown at another time and possibly on another thread by calling the ExceptionDispatchInfo.Throw method. The exception is thrown as if it had flowed from the point where it was captured to the point where the Throw method is called.
代码如下:

ExceptionDispatchInfo.Capture(ex.InnerException).Throw();

此时异常如下,原始异常被追加上去了:

exception stacktrace:
   at PluginTest.Processor.Runner(DateTime dateTime)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Canary.PluginRunner.ExecuteCore() in xxx\Canary\PluginRunner.cs:line 32
   at Canary.Program.Main(String[] args) in xxx\Canary\Program.cs:line 19

SO大佬在2010年指出这是一个Windows CLR的限制:
This is a well known limitation in the Windows version of the CLR. It uses Windows' built-in support for exception handling (SEH). Problem is, it is stack frame based and a method has only one stack frame. You can easily solve the problem by moving the inner try/catch block into another helper method, thus creating another stack frame. Another consequence of this limitation is that the JIT compiler won't inline any method that contains a try statement.

那么Linux下呢, 我在WSL下看了看是确实没有这个问题.

➜  CanaryNetCore git:(master) ✗ dotnet run

Unhandled Exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Exception: Hold on..
   at PluginTestCore.Processor.Runner(DateTime dateTime)
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at CanaryNetCore.PluginRunner.ExecuteCore() in /mnt/c/DevLab/Canary/CanaryNetCore/PluginRunner.cs:line 33
   at CanaryNetCore.Program.Main(String[] args) in /mnt/c/DevLab/Canary/CanaryNetCore/Program.cs:line 18

➜  CanaryNetCore git:(master) ✗ cat cat PluginRunner.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading.Tasks;

namespace CanaryNetCore
{
    class PluginRunner
    {
        public void ExecuteCore()
        {
            Assembly assembly = Assembly.LoadFrom("PluginTestCore.dll");

            object obj = assembly.CreateInstance("PluginTestCore.Processor");
            MethodInfo m = obj.GetType().GetMethod("Runner");

            if (m != null)
            {
                object[] methodParams = new object[] { DateTime.Now };
                try
                {
                    var result = m.Invoke(obj, methodParams);
                }
                catch (Exception ex)
                {
                    //if (ex.InnerException != null)
                    //{
                    //    ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
                    //}
                    throw;
                }

            }
        }
    }
}

第二种方法是直接throw,在外面处理InnerException, 由于我不想修改外部代码, 所以使用了第一种方法.
The recommended way to re-throw an exception is to simply use the throw statement in C# and the Throw statement in Visual Basic without including an expression. This ensures that all call stack information is preserved when the exception is propagated to the caller.

参考:
1. https://stackoverflow.com/questions/57383/in-c-how-can-i-rethrow-innerexception-without-losing-stack-trace
2. https://msdn.microsoft.com/en-us/library/system.runtime.exceptionservices.exceptiondispatchinfo%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396
3. https://msdn.microsoft.com/en-us/library/system.exception(v=vs.110).aspx

595mm in daily life

595mm

最近搬家,需要买一些家电.由于厨房只有一道约60公分的空间,在买家电的时候关注了一下尺寸. 惊奇的发现很多家电都有一个相似的尺寸:595mm.

  • 单开门冰箱
  • 滚筒洗衣机,干衣机
  • 洗碗机

我想这肯定是装修时有意如此设计的,留下一道60公分的空间来放置家电.也就是说多年前这个宽度已经成为行业标准,且各个家电行业的设计都在遵循这个标准. 由此想到计算机的硬件接口设计也是如此, 外部接口USB/SATA/VGA&HDMI多年只在进化且保持向下兼容.

不过计算机内部的接口就没有那么’友好’了,CPU/内存针脚隔代基本不兼容. 反之推想,各种电器内部也肯定经过了多重改进/改良.

都是在条条框框里面进化.