Open Source Adventures: Episode 48: Extracting DLC data from BATTLETECH
Tags: ruby, gamedev
Published: 2022-04-30 00:55:54

I tried various tools to extract data from `heavymetal` DLC.

### DisUnity

First I tried [DisUnity](https://github.com/ata4/disunity) - but that's an abandoned project, and it doesn't work with current version.

### Unity Asset Bundle Extractor

Next I tried [Unity Asset Bundle Extractor](https://github.com/SeriousCache/UABE) which is very nonintuitive but it extracted what I want. "Select All" doesn't work, I needed to select first item, then shift click on the last item. Then I pressed "Export Raw".

It unfortunately created files like: `weapon_autocannon_lb10x_0_stock.json-CAB_81519cd4dc355e215c567f814d1dd677--9084477644472635856.dat` instead of `weapon/autocannon_lb10x_0_stock.json`.

They also some weird crap at start and end of each file. It wouldn't be too difficult to strip, but I hoped for just the proper data.

"Dump as text file" and "Dump as json file" options were even worse. Finally "Plugins > Export to .txt" worked.

Unity Asset Bundle Extractor has all information necessary to get it right, it just choses not to.

### Organizing the files

The files still had stupid names like `weapon_autocannon_lb10x_0_stock.json-CAB_81519cd4dc355e215c567f814d1dd677--9084477644472635856.txt`.

So I did a quick:

```
$ rename 's/-CAB_.*//' *.txt
```

After that I moved weapon files to `heavymetal/weapon/*.json`. I might need to organize other files like ammo etc. later.

Fortunately that's the only DLC with extra weapons. If I wanted to analyze all mechs, I'd need to repeat the same steps with other DLCs.

### Script

```ruby
#!/usr/bin/env ruby

require "json"
require "memoist"
require "pathname"
require "pry"

class Weapon
  extend Memoist
  attr_reader :data

  def initialize(path)
    @data = JSON.parse(path.read)
  end

  memoize def name
    bonuses = [bonus_a, bonus_b].compact
    if bonuses.empty?
      base_name
    else
      "#{base_name} (#{bonuses.join(", ")})"
    end
  end

  memoize def base_name
    [
      data["Description"]["Name"],
      data["Description"]["UIName"],
    ].compact.last.gsub(" +", "+")
  end

  memoize def bonus_a
    data["BonusValueA"] == "" ? nil : data["BonusValueA"].gsub(/[a-z]\K\./, "")
  end

  memoize def bonus_b
    data["BonusValueB"] == "" ? nil : data["BonusValueB"].gsub(/[a-z]\K\./, "")
  end

  def category
    @data["Category"]
  end

  def tonnage
    @data["Tonnage"]
  end

  def damage
    shots * base_damage
  end

  def base_damage
    @data["Damage"]
  end

  def shots
    @data["ShotsWhenFired"]
  end

  def heat
    @data["HeatGenerated"]
  end

  def heat_tonnage
    heat / 3.0
  end

  def ammo_tonnage
    # TODO
    0
  end

  def total_tonnage
    tonnage + heat_tonnage + ammo_tonnage
  end

  def ammo_category
    @data["ammoCategoryID"]
  end

  def purchasable?
    @data["Description"]["Purchasable"]
  end
end

class BattleTechGame
  extend Memoist

  def initialize(game_root, *dlc_roots)
    @game_root = Pathname(game_root)
    @dlc_roots = dlc_roots.map{|path| Pathname(path)}
  end

  memoize def data_root
    @game_root + "BattleTech_Data/StreamingAssets/data"
  end

  memoize def weapon_files
    [data_root, *@dlc_roots]
      .flat_map{|root| root.glob("weapon/*.json")}
      .select{|n| n.basename.to_s != "WeaponTemplate.json"}
  end

  memoize def weapons
    weapon_files.map{|path| Weapon.new(path)}
  end

  def print_weapons_report
    weapons
      .select{|w| w.category != "Melee" and w.name != "AI Laser"}
      .sort_by{|w|
        [w.damage.to_f / w.total_tonnage, w.damage, w.name]
      }
      .reverse
      .each{|w|
        puts "* #{w.name}: #{w.damage} / #{w.total_tonnage.round(2)}"
      }
  end
end

game = BattleTechGame.new(*ARGV)
game.print_weapons_report
```

To run the scirpt I use `./weapons /path/to/battletech /path/to/heavymetal`

### Result

Here's the result. As the script still doesn't count ammo, it overvalues ammo using weapons. It's also quite confused by any weapon that has special effects like all those TAGs and NARCs and COILs on the end of the list. It might be best to just exclude them the analysis.

* MG++ (+ 5 Shots, - 0.5 Ton): 30 / 0.0
* MG++ (+ 50% Crit, - 0.5 Ton): 15 / 0.0
* MG+ (+ 5 Shots): 30 / 0.5
* MG+ (+ 25% Crit): 15 / 0.5
* MG: 15 / 0.5
* LB 2-X++ (+ 2 Dmg, + 50% Crit): 72 / 5.67
* S Laser+++ (+ 10 Dmg, + 50% Crit): 30 / 2.5
* S Laser++ (+ 10 Dmg): 30 / 2.5
* LB 2-X+ (+ 1 Dmg, + 25% Crit): 60 / 5.67
* ER S Laser++ (+ 10 Dmg): 40 / 3.83
* S Pulse++ (+ 10 Dmg, - 2 Heat): 45 / 4.33
* SRM6+++ (+ 4 Dmg, + 2 StbDmg): 72 / 7.0
* SRM6++ (+ 4 Dmg): 72 / 7.0
* SRM4+++ (+ 4 Dmg, + 2 StbDmg): 48 / 4.67
* SRM4++ (+ 4 Dmg): 48 / 4.67
* SRM2+++ (+ 4 Dmg, + 2 StbDmg): 24 / 2.33
* SRM2++ (+ 4 Dmg): 24 / 2.33
* S Laser++ (+ 5 Dmg, + 25% Crit): 25 / 2.5
* S Laser+ (+ 5 Dmg): 25 / 2.5
* Flamer+ (* 4 Ammo, + 5 Dmg): 10 / 1.0
* UAC/2++ (+ 10 Dmg, - 2 Tons): 70 / 7.67
* ER S Laser+ (+ 5 Dmg): 35 / 3.83
* SRM4++ (+ 2 Dmg, + 1 StbDmg): 40 / 4.67
* SRM4+ (+ 2 Dmg): 40 / 4.67
* S Pulse+ (+ 5 Dmg, - 1 Heat): 40 / 4.67
* SRM2++ (+ 2 Dmg, + 1 StbDmg): 20 / 2.33
* SRM2+ (+ 2 Dmg): 20 / 2.33
* UAC/20++ (+ 20 Dmg, - 3 Tons): 240 / 28.0
* SRM6++ (+ 2 Dmg, + 1 StbDmg): 60 / 7.0
* SRM6+ (+ 2 Dmg): 60 / 7.0
* LB 2-X: 48 / 5.67
* S Laser++ (+ 50% Crit): 20 / 2.5
* S Laser++ (+ 3 Acc): 20 / 2.5
* S Laser+ (+ 25% Crit): 20 / 2.5
* S Laser+ (+ 1 Acc): 20 / 2.5
* S Laser: 20 / 2.5
* ER S Laser: 30 / 3.83
* LRM15++ (+ 2 Dmg): 90 / 11.67
* LRM20++ (+ 2 Dmg): 120 / 16.0
* LRM5++ (+ 2 Dmg): 30 / 4.0
* UAC/20+ (+ 10 Dmg, - 1.5 Tons): 220 / 29.5
* UAC/5++ (- 2 Recoil, - 2 Tons): 90 / 12.33
* LB 10-X++ (+ 2 Dmg, - 1 Ton): 96 / 13.33
* LRM10++ (+ 2 Dmg): 60 / 8.33
* Snub PPC++ (+ 10 Dmg): 125 / 17.67
* S Pulse: 35 / 5.0
* M Laser++ (+ 10 Dmg): 35 / 5.0
* UAC/2+ (+ 5 Dmg, - 1 Ton): 60 / 8.67
* SRM4++ (+ 50% Crit): 32 / 4.67
* SRM4+ (+ 25% Crit): 32 / 4.67
* SRM4: 32 / 4.67
* SRM2++ (+ 50% Crit): 16 / 2.33
* SRM2+ (+ 25% Crit): 16 / 2.33
* SRM2: 16 / 2.33
* SRM6++ (+ 50% Crit): 48 / 7.0
* SRM6+ (+ 25% Crit): 48 / 7.0
* SRM6: 48 / 7.0
* M Pulse++ (- 4 Heat, + 1 Acc): 50 / 7.33
* UAC/5+ (- 1 Recoil, - 1 Ton): 90 / 13.33
* UAC/10++ (+ 10 StbDmg, - 3 Tons): 120 / 18.0
* LB 5-X++ (+ 4 StbDmg, - 1 Ton): 60 / 9.0
* UAC/20: 200 / 31.0
* LB 20-X++ (+ 4 StbDmg, - 2 Tons): 120 / 18.67
* LRM15+ (+ 1 Dmg): 75 / 11.67
* LB 10-X+ (+ 1 Dmg, - 0.5 Ton): 88 / 13.83
* LB 5-X+ (+ 2 StbDmg, - 0.5 Ton): 60 / 9.5
* UAC/5: 90 / 14.33
* LRM20+ (+ 1 Dmg): 100 / 16.0
* LRM5+ (+ 1 Dmg): 25 / 4.0
* UAC/10+ (+ 5 StbDmg, - 1.5 Tons): 120 / 19.5
* LB 20-X+ (+ 2 StbDmg, - 1 Ton): 120 / 19.67
* LB 5-X: 60 / 10.0
* LRM10+ (+ 1 Dmg): 50 / 8.33
* M Laser+ (+ 5 Dmg): 30 / 5.0
* ER M Laser++ (+ 10 Dmg): 45 / 7.67
* LB 20-X: 120 / 20.67
* UAC/10: 120 / 21.0
* Snub PPC+ (+ 5 Dmg): 100 / 17.67
* M Pulse+ (- 2 Heat): 45 / 8.0
* LB 10-X: 80 / 14.33
* AC/20+++ (+ 20 Dmg, + 20 StbDmg): 120 / 22.0
* AC/20++ (+ 20 Dmg): 120 / 22.0
* Snub PPC++ (- 10 Heat): 75 / 14.33
* ER M Laser+ (+ 5 Dmg): 40 / 7.67
* UAC/2: 50 / 9.67
* AC/5++ (+ 10 Dmg): 55 / 10.67
* LRM15+++ (+ 50% Crit, + 2 StbDmg): 60 / 11.67
* LRM15++ (+ 50% Crit): 60 / 11.67
* LRM15++ (+ 25% Crit, + 1 StbDmg): 60 / 11.67
* LRM15++ (+ 2 StbDmg): 60 / 11.67
* LRM15+ (+ 25% Crit): 60 / 11.67
* LRM15+ (+ 1 StbDmg): 60 / 11.67
* LRM15: 60 / 11.67
* Gauss Rifle++ (- 2 Tons, - 1 Slot): 75 / 14.67
* Gauss Rifle+ (- 2 Tons): 75 / 14.67
* AC/20++ (+ 10 Dmg, + 10 StbDmg): 110 / 22.0
* AC/20+ (+ 10 Dmg): 110 / 22.0
* LRM20+++ (+ 50% Crit, + 2 StbDmg): 80 / 16.0
* LRM20++ (+ 50% Crit): 80 / 16.0
* LRM20++ (+ 25% Crit, + 1 StbDmg): 80 / 16.0
* LRM20++ (+ 2 StbDmg): 80 / 16.0
* LRM20+ (+ 25% Crit): 80 / 16.0
* LRM20+ (+ 1 StbDmg): 80 / 16.0
* LRM20: 80 / 16.0
* M Laser+++ (+ 3 Acc, + 50% Crit): 25 / 5.0
* M Laser++ (+3 Acc): 25 / 5.0
* M Laser++ (+ 50% Crit): 25 / 5.0
* M Laser++ (+ 1 Acc, + 25% Crit): 25 / 5.0
* M Laser+ (+1 Acc): 25 / 5.0
* M Laser+ (+ 25% Crit): 25 / 5.0
* M Laser: 25 / 5.0
* LRM5+++ (+ 50% Crit, + 2 StbDmg): 20 / 4.0
* LRM5++ (+ 50% Crit): 20 / 4.0
* LRM5++ (+ 25% Crit, + 1 StbDmg): 20 / 4.0
* LRM5++ (+ 2 StbDmg): 20 / 4.0
* LRM5+ (+ 25% Crit): 20 / 4.0
* LRM5+ (+ 1 StbDmg): 20 / 4.0
* LRM5: 20 / 4.0
* Flamer+++ (* 40 Ammo): 5 / 1.0
* Flamer++ (* 4 Ammo, + 5 Dmg (H)): 5 / 1.0
* Flamer (* 4 Ammo): 5 / 1.0
* LRM10+++ (+ 50% Crit, + 2 StbDmg): 40 / 8.33
* LRM10++ (+ 50% Crit): 40 / 8.33
* LRM10++ (+ 25% Crit, + 1 StbDmg): 40 / 8.33
* LRM10++ (+ 2 StbDmg): 40 / 8.33
* LRM10+ (+ 25% Crit): 40 / 8.33
* LRM10+ (+ 1 StbDmg): 40 / 8.33
* LRM10: 40 / 8.33
* AC/2++ (+ 10 Dmg): 35 / 7.33
* Snub PPC+ (- 5 Heat): 75 / 16.0
* AC/5+ (+ 5 Dmg): 50 / 10.67
* M Pulse: 40 / 8.67
* ER M Laser: 35 / 7.67
* AC/20+++ : 100 / 22.0
* AC/20++ (+ 50% Crit): 100 / 22.0
* AC/20++ (+ 4 Acc): 100 / 22.0
* AC/20++ (+ 20 StbDmg): 100 / 22.0
* AC/20+ (+ 25% Crit): 100 / 22.0
* AC/20+ (+ 2 Acc): 100 / 22.0
* AC/20+ (+ 10 StbDmg): 100 / 22.0
* AC/20: 100 / 22.0
* L Laser+++ (+ 10 Dmg, + 3 Acc): 50 / 11.0
* L Laser++ (+ 10 Dmg): 50 / 11.0
* Gauss Rifle: 75 / 16.67
* AC/10++ (+ 10 Dmg): 70 / 16.0
* Snub PPC++ (+ 10 StbDmg): 75 / 17.67
* Snub PPC+ (+ 5 StbDmg): 75 / 17.67
* Snub PPC: 75 / 17.67
* AC/5+++ (+ 4 Acc, + 20 StbDmg): 45 / 10.67
* AC/5++ (+ 50% Crit): 45 / 10.67
* AC/5++ (+ 4 Acc): 45 / 10.67
* AC/5++ (+ 20 StbDmg): 45 / 10.67
* AC/5++ (+ 2 Acc, + 10 StbDmg): 45 / 10.67
* AC/5+ (+ 25% Crit): 45 / 10.67
* AC/5+ (+ 2 Acc): 45 / 10.67
* AC/5+ (+ 10 StbDmg): 45 / 10.67
* AC/5: 45 / 10.67
* L Laser++ (+ 5 Dmg, + 1 Acc): 45 / 11.0
* L Laser+ (+ 5 Dmg): 45 / 11.0
* AC/2+ (+ 5 Dmg): 30 / 7.33
* L Pulse++ (+ 10 Dmg, - 1 Ton): 65 / 16.0
* AC/10+ (+ 5 Dmg): 65 / 16.0
* ER L Laser++ (+ 5 Dmg, - 1 Ton): 50 / 12.33
* L Pulse+ (+ 5 Dmg, - 1 Ton): 60 / 16.0
* AC/10+++ (+ 50% Crit, + 20 StbDmg): 60 / 16.0
* AC/10++ (+ 50% Crit): 60 / 16.0
* AC/10++ (+ 4 Acc): 60 / 16.0
* AC/10++ (+ 25% Crit, + 10 StbDmg): 60 / 16.0
* AC/10++ (+ 20 StbDmg): 60 / 16.0
* AC/10+ (+ 25% Crit): 60 / 16.0
* AC/10+ (+ 2 Acc): 60 / 16.0
* AC/10+ (+ 10 StbDmg): 60 / 16.0
* AC/10: 60 / 16.0
* ER L Laser+ (+ 5 Dmg): 50 / 13.33
* L Laser++ (+ 50% Crit): 40 / 11.0
* L Laser++ (+ 3 Acc): 40 / 11.0
* L Laser+ (+ 25% Crit): 40 / 11.0
* L Laser+ (+ 1 Acc): 40 / 11.0
* L Laser: 40 / 11.0
* AC/2+++ (+ 4 Acc, + 50% Crit): 25 / 7.33
* AC/2++ (+ 50% Crit): 25 / 7.33
* AC/2++ (+ 4 Acc): 25 / 7.33
* AC/2++ (+ 20 StbDmg): 25 / 7.33
* AC/2++ (+ 2 Acc, + 25% Crit): 25 / 7.33
* AC/2+ (+ 25% Crit): 25 / 7.33
* AC/2+ (+ 2 Acc): 25 / 7.33
* AC/2+ (+ 10 StbDmg): 25 / 7.33
* AC/2: 25 / 7.33
* ER L Laser: 45 / 13.33
* ER PPC++ (+ 10 Dmg, - 1 Ton): 70 / 21.0
* L Pulse: 55 / 17.0
* PPC++ (+ 10 Dmg): 60 / 18.67
* ER PPC+ (+ 5 Dmg, - 1 Ton): 65 / 21.0
* PPC+ (+ 5 Dmg): 55 / 18.67
* ER PPC: 60 / 22.0
* PPC++ (+ 4 Acc): 50 / 18.67
* PPC++ (+ 30 StbDmg): 50 / 18.67
* PPC+ (+ 2 Acc): 50 / 18.67
* PPC+ (+ 15 StbDmg): 50 / 18.67
* PPC: 50 / 18.67
* AC/10: 30 / 21.0
* COIL-L (Dmg X Evasion): 35 / 27.33
* COIL-M (Dmg X Evasion): 25 / 19.67
* COIL-S (Dmg X Evasion): 15 / 12.0
* Infernos++ (* 8 Volleys, + 10 Dmg (H)): 4 / 4.0
* Infernos+ (* 8 Volleys, + 5 Dmg (H)): 4 / 4.0
* Infernos (* 8 Volleys): 4 / 4.0
* AC/10: 30 / 32.0
* TAG++ (+ 20% Dmg Mark, * Energy, Ballistic): 1 / 2.67
* TAG+ (+ 15% Dmg Mark, * Energy, Ballistic): 1 / 2.67
* TAG (+ 10% Dmg Mark, * Energy, Ballistic): 1 / 2.67
* Narc Beacon++ (+ 75% Dmg Mark, * Missile): 1 / 3.0
* Narc Beacon+ (+ 50% Dmg Mark, * Missile): 1 / 3.0
* Narc Beacon (+ 25% Dmg Mark, * Missile): 1 / 3.0

### Story so far

[All the code is on GitHub](https://github.com/taw/open-source-adventures).

### Coming next

In the next few episodes, we'll finish the script by including ammo in the count.