Skip to content

Instantly share code, notes, and snippets.

@ryandagg
Last active June 13, 2025 23:41
Show Gist options
  • Save ryandagg/5d993bd4ecaa8c0afa0b1e533bc15a2f to your computer and use it in GitHub Desktop.
Save ryandagg/5d993bd4ecaa8c0afa0b1e533bc15a2f to your computer and use it in GitHub Desktop.
decode Diablo II's CubeMain.bin, write to JSON
# modified from https://gist.github.com/jankowskib/f886f087d9cb1f578284
# works for at least...
# % ruby --version
# ruby 2.7.7p221 (2022-11-24 revision 168ec2b1e5) [arm64-darwin24]
# usage:
# ruby parser-json.rb ./cubemain.bin somewhere/else/cubemain.json
# ^ assumes you named this file "parser-json.rb", otherwise change the command to match
=begin
getting the JSON back to a .txt file is involved, as it appears you need to parse various other .bin files to translate from various ids to the strings.
example cell from a .txt file => .bin => .json
the "output" column's value in cubemain.txt is "Cow Portal", but the output object parsed from the .bin is below. I have no idea how to interpret this yet.
{
"item_flags": "0",
"item_type": "0",
"item": "0",
"item_id": "0",
"param": "0",
"output_type": "1",
"lvl": "0",
"p_lvl": "0",
"i_lvl": "0",
"prefix_id": [
"0",
"0",
"0"
],
"suffix_id": [
"0",
"0",
"0"
],
"mods": [
{
"mod": "4294967295",
"mod_param": "0",
"mod_min": "0",
"mod_max": "0",
"mod_chance": "0"
},
...repeats
]
}
=end
require 'bindata'
require 'json'
require 'fileutils'
class CubeMainInput < BinData::Record # size 0x08
uint8 :input_flags #0x00
uint8 :item_type #0x01
uint16le :item #0x02
uint16le :item_id #0x04
uint8 :quality #0x06
uint8 :quantity #0x07
end
class CubeMainOutputMod < BinData::Record # size 0xC
uint32le :mod #0x00
uint16le :mod_param #0x04
uint16le :mod_min #0x06
uint16le :mod_max #0x08
uint16le :mod_chance #0x0a
end
class CubeMainOutput < BinData::Record # size 0x54
uint8 :item_flags #0x00
uint8 :item_type #0x01
uint16le :item #0x02
uint16le :item_id #0x04
uint16le :param #0x06
uint8 :output_type #0x08 edited from "type" as that is reserved in this version of Ruby
uint8 :lvl #0x09
uint8 :p_lvl #0x0a
uint8 :i_lvl #0x0b
array :prefix_id, :type => :uint16le, :initial_length => 3 #0x0c
array :suffix_id, :type => :uint16le, :initial_length => 3 #0x12
array :mods, :type => :cube_main_output_mod, :initial_length => 5 #0x18
end
class CubeMainBinRecord < BinData::Record # size 0x148
uint8 :enabled #0x00
uint8 :ladder #0x01
uint8 :mindiff #0x02
uint8 :class_id #0x03
uint32le :op #0x04
uint32le :param #0x08
uint32le :value_id #0x0c
uint16le :numinputs #0x10
uint16le :version #0x12
array :inputs, :type => :cube_main_input, :initial_length => 7
array :outputs, :type => :cube_main_output, :initial_length => 3
end
class CubeMainBin < BinData::Record # size 4+
uint32le :record_count
array :records, :type => :cube_main_bin_record, :initial_length => :record_count
end
begin
f = ARGV[0] unless ARGV[0].nil?
output_path = ARGV[1] unless ARGV[1].nil?
bin = File.read(f)
records = CubeMainBin.read(bin).records
# Helper to recursively convert BinData objects to hashes
def bin_to_hash(obj)
if obj.is_a?(BinData::Array) || obj.is_a?(Array)
obj.map { |el| bin_to_hash(el) }
elsif obj.is_a?(BinData::Record)
obj.field_names.each_with_object({}) do |field, h|
h[field.to_s] = bin_to_hash(obj.send(field))
end
else
obj
end
end
json_records = records.map { |rec| bin_to_hash(rec) }
input_basename = File.basename(f, File.extname(f))
json_file = output_path ? output_path : File.join('assets', 'outputs', "#{input_basename}.json")
FileUtils.mkdir_p(File.dirname(json_file))
File.write(json_file, JSON.pretty_generate(json_records))
puts "Wrote #{json_file} with #{json_records.size} records."
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment