diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..2444a56 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'bin_tools' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..94537d0 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,13 @@ +GEM + remote: https://rubygems.org/ + specs: + bin_tools (0.0.3) + +PLATFORMS + ruby + +DEPENDENCIES + bin_tools + +BUNDLED WITH + 1.16.1 diff --git a/binwriter.rb b/binwriter.rb deleted file mode 100644 index 0288a69..0000000 --- a/binwriter.rb +++ /dev/null @@ -1,123 +0,0 @@ -16# 8 16 32 64 -# C S L Q - unsigned -# c s l q - unsigned -# < LE -# > BE -# A - binary string -# z - null terminated -# H - hex string - - -class BinWriter - def initialize fn - @f = File.new fn, "wb" - end - - def self.open fn, &block - f = BinWriter.new fn - block.call f - f.close - end - - def seek pos - @f.seek pos - end - - def tell - @f.tell - end - - def write_str str - @f.write str - end - - def write_str_varlen str - write_varlen_le str.size - @f.write str - end - - def write_byte val - @f.write [val].pack("C") - end - - def write_bool val - write_byte val ? 1 : 0 - end - - def write_u16_le val - @f.write [val].pack("S<") - end - - def write_s16_le val - @f.write [val].pack("s<") - end - - def write_u16_be val - @f.write [val].pack("S>") - end - - def write_u32_le val - @f.write [val].pack("L<") - end - - def write_s32_le val - @f.write [val].pack("l<") - end - - def write_u32_be val - @f.write [val].pack("L>") - end - - def write_f32_le val - @f.write [val].pack("e") - end - - def write_u24_be val - write_byte val >> 16 - write_u16_be val & 0xFFFF - end - - def write_bin str - @f.write str - end - - def write_varlen_be val - out = [val & 0x7F] - val = val >> 7 - while val > 0 - out << (val & 0x7f) + 0x80 - val = val >> 7 - end - out.reverse.each do |x| - write_byte x - end - end - - def write_varlen_le val - out = [val & 0x7F] - val = val >> 7 - while val > 0 - out << (val & 0x7f) - val = val >> 7 - end - out.each_with_index do |x, i| - write_byte(x + ((i == out.size - 1) ? 0 : 0x80)) - end - end - - def write_binswap str - togo = str.size - pos = 0 - while togo > 0 - r = @rom[pos..(pos+3)].unpack('L>') - d = r.pack('L<') - write_bin d - pos += 4 - togo -= 4 - end - end - - def close - @f.close - end -end diff --git a/celeste_map.rb b/celeste_map.rb index 1febfff..29ae13f 100644 --- a/celeste_map.rb +++ b/celeste_map.rb @@ -1,9 +1,11 @@ -require './rom' -require './binwriter' +require 'bin_tools' require 'json' + # Value Types # :boolean, :u8, :s16, :s32, :float, :lookup, :bin, :rle +$ElementAutoParser = {} + class Element attr_accessor :package, :name, :attributes, :children, :attributes_value_types; def initialize @@ -37,18 +39,18 @@ class Element case value_type when :boolean if v - "#{pres}#{k}" + "#{pres}#{k}(#{value_type})" else - "#{pres}#{k}={#{v}}" + "#{pres}#{k}(#{value_type})={#{v}}" end when :u8, :s16, :s32, :float - "#{pres}#{k}={#{v}}" + "#{pres}#{k}(#{value_type})={#{v}}" when :lookup - "#{pres}#{k}=\"#{v}\"" + "#{pres}#{k}(#{value_type})=\"#{v}\"" when :bin - "#{pres}#{k}={bin#{v.inspect}}" + "#{pres}#{k}(#{value_type})={#{v.inspect}}" when :rle - "#{pres}#{k}={rle#{v.inspect}}" + "#{pres}#{k}(#{value_type})={#{v.inspect}}" end end.join("\n") end @@ -64,6 +66,35 @@ class Element def [] name attributes[name.to_s] end + def set_attribute name, value_type, value + attributes[name.to_s] = value + attributes_value_types[name.to_s] = value_type + end + def self.auto_type num + if (num >= 0 && num <= 255) + :u8 + elsif num >= -32768 && num < 32768 + :s16 + else + :s32 + end + end + + def set_num_attribute name, num + num = num.to_i(16) if num.is_a? String + set_attribute name, self.class.auto_type(num), num + end + + + + def set_lookup_attribute name, str + set_attribute name, :lookup, str + end + + def set_boolean_attribute name, val + set_attribute name, :boolean, val + end + def to_h { 'name' => name, @@ -91,18 +122,51 @@ class Element element.package = obj['package'] element.attributes = obj['attributes'] element.attributes_value_types = obj['attribute_types'] - element.children = obj['children'].map { |c| from_h c } + extra_children = [] + element.children = obj['children'].map do |c| + parser = nil + c.each do |k, v| + parser = $ElementAutoParser[k] if $ElementAutoParser[k] + end + z = if parser + parser.from_h c + else + if c.has_key?('repeat') + clones = (0..c['repeat']).each do |rr| + nn = c.clone + nn['attributes'] = nn['attributes'].clone + nn['attribute_types'] = nn['attribute_types'].clone + c['repeat_attributes'].each do |k, v| + nn['attributes'][k] += (rr + 1) * v + nn['attribute_types'][k] = 's16' + end + extra_children << from_h(nn) + end + end + from_h c + end + if z.is_a? Array + z.each_with_index do |zz, i| + extra_children << zz if i > 0 + end + z.first + else + z + end + end + element.children += extra_children element end end + class CelesteMap attr_accessor :debug, :rom, :package, :string_lookup, :root, :writer def initialize fn, fmt: :bin, debug: false @debug = debug case fmt when :bin - @rom = ROM.from_file(fn) + @rom = BinTools::Reader.from_file(fn) raise "Not a celeste map" unless rom.read_str_varlen == 'CELESTE MAP' @package = rom.read_str_varlen @string_lookup = (0...rom.read_u16_le).map { rom.read_str_varlen } @@ -119,7 +183,7 @@ class CelesteMap end def write fn - @writer = BinWriter.new(fn) + @writer = BinTools::BinWriter.new(fn) @string_lookup = root.strings writer.write_str_varlen 'CELESTE MAP' writer.write_str_varlen root.package @@ -290,3 +354,307 @@ class CelesteMap puts s if @debug end end + +class NodeElement < Element + def self.with_xy x, y + e = self.new + e.name = 'node' + e.set_num_attribute 'x', x + e.set_num_attribute 'y', y + e + end +end + +class EntityElement < Element + def self.gen_id key + @gen_id = {} unless @gen_id + @gen_id[key] = 0 unless @gen_id[key] + @gen_id[key] = @gen_id[key] + 1 + @gen_id[key] + end + + def self.reset_id key + @gen_id = {} unless @gen_id + @gen_id[key] = 0 + end + + def set_gen_id key + set_num_attribute 'id', self.class.gen_id(key) + end + + def set_auto_name obj + self.name = obj['__entity'] + end + + def set_auto_num_attribute obj, name + set_num_attribute name, obj[name] + end + + def set_auto_lookup_attribute obj, name + set_lookup_attribute name, obj[name] + end + + def set_auto_boolean_attribute obj, name + set_boolean_attribute name, obj[name] + end + + def set_auto_xy obj, oX = 0, oY = 0 + x = obj['x'] + y = obj['y'] + x = x.to_i(16) if x.is_a? String + y = y.to_i(16) if y.is_a? String + + set_num_attribute 'x', x + oX + set_num_attribute 'y', y + oY + end + + def set_auto_width obj + set_auto_num_attribute obj, 'width' + end + + def set_auto_height obj + set_auto_num_attribute obj, 'height' + end + + def set_auto_wh obj + set_auto_width obj + set_auto_height obj + end + + def set_origin x, y + set_num_attribute 'originX', x + set_num_attribute 'originY', y + end + + def self.parse_player obj + e = self.new + e.set_auto_name obj + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_num_attribute 'width', 8 + e.set_origin 4, 8 + e + end + + def self.parse_badeline_chaser obj + e = self.new + e.name = 'darkChaser' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_origin 4, 8 + e + end + + def self.parse_booster obj + e = self.new + e.set_auto_name obj + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_boolean_attribute obj, 'red' + e.set_origin 4, 4 + e + end + + def self.parse_move_block obj + e = self.new + e.name = 'moveBlock' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_wh obj + e.set_auto_lookup_attribute obj, 'direction' + e.set_auto_boolean_attribute obj, 'canSteer' + e.set_auto_boolean_attribute obj, 'fast' + e.set_origin 0, 0 + e + end + + def self.parse_black_gem obj + e = self.new + e.name = 'blackGem' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_boolean_attribute obj, 'removeCameraTriggers' + e.set_origin 6, 6 + e + end + + def self.parse_golden_berry obj + e = self.new + e.name = 'goldenBerry' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_origin 8, 8 + e + end + + def self.parse_dream_block obj + e = self.new + e.name = 'dreamBlock' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_wh obj + e.set_auto_boolean_attribute obj, 'fastMoving' + e.set_origin 0, 0 + e + end + + def self.parse_kevin obj + e = self.new + e.name = 'crushBlock' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_wh obj + e.set_auto_lookup_attribute obj, 'axes' + e.set_auto_boolean_attribute obj, 'chillout' + e.set_origin 0, 0 + e + end + + def self.parse_water obj + e = self.new + e.name = 'water' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_wh obj + e.set_auto_boolean_attribute obj, 'steamy' + e.set_auto_boolean_attribute obj, 'hasBottom' + e.set_origin 0, 0 + e + end + + def self.parse_jump_thru obj + e = self.new + e.name = 'jumpThru' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_width obj + e.set_origin 0, 0 + e + end + + def self.parse_zip_mover obj + e = self.new + e.name = 'zipMover' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_wh obj + e.set_origin 0, 0 + e.children << NodeElement.with_xy(obj['x2'], obj['y2']) + e + end + + def self.parse_switch_gate obj + e = self.new + e.name = 'switchGate' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_wh obj + e.set_auto_boolean_attribute obj, 'persistent' + e.set_auto_lookup_attribute obj, 'sprite' + e.set_origin 0, 0 + e.children << NodeElement.with_xy(obj['x2'], obj['y2']) + e + end + + def self.parse_bumper obj + e = self.new + e.name = 'bigSpinner' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_origin 16, 16 + e + end + + def self.parse_cloud obj + e = self.new + e.set_auto_name obj + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_boolean_attribute obj, 'fragile' + e.set_origin 16, 0 + e + end + + def self.parse_spikes obj + e = self.new + e.name = 'spikes' + obj['direction'] + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_auto_lookup_attribute obj, 'type' + case obj['direction'] + when 'Up' + e.set_auto_width obj + e.set_origin 0, 4 + when 'Down' + e.set_auto_width obj + e.set_origin 0, 0 + when 'Left' + e.set_auto_height obj + e.set_origin 4, 0 + when 'Right' + e.set_auto_height obj + e.set_origin 0, 0 + end + e + end + + def self.parse_touch_switch obj + e = self.new + e.name = 'touchSwitch' + e.set_gen_id 'entity' + e.set_auto_xy obj + e.set_origin 4, 4 + e + end + + def self.parse_spinner obj + nY = obj['nY'] || 1 + nX = obj['nX'] || 1 + dY = obj['dY'] || 16 + dX = obj['dX'] || 16 + elements = [] + nY.times do |iY| + nX.times do |iX| + e = self.new + e.set_auto_name obj + e.set_gen_id 'entity' + e.set_auto_boolean_attribute obj, 'attachToSolid' + e.set_auto_xy obj, iX * dX, iY * dY + e.set_origin 8, 8 + elements << e + end + end + elements + end + + def self.from_h obj + self.send("parse_#{obj['__entity']}", obj) + end +end + +$ElementAutoParser['__entity'] = EntityElement + +class SolidsElement < Element + def set_auto_xy obj, oX = 0, oY = 0 + x = obj['offsetX'] || 0 + y = obj['offsetY'] || 0 + x = x.to_i(16) if x.is_a? String + y = y.to_i(16) if y.is_a? String + + set_num_attribute 'offsetX', x + oX + set_num_attribute 'offsetY', y + oY + end + + def self.from_h obj + e = self.new + e.name = 'solids' + e.set_auto_xy obj + inner = obj['map'].map do |str| + str.split('').map { |ch| ch.ord == 32 && '0'.ord || ch.ord } + [10] + end.flatten + e.set_attribute 'innerText', :rle, inner + e + end +end + +$ElementAutoParser['__solids'] = SolidsElement diff --git a/json2map.rb b/json2map.rb index bf63d4a..9513efe 100644 --- a/json2map.rb +++ b/json2map.rb @@ -3,6 +3,9 @@ require "./celeste_map" ARGV.each do |fn| base = File.basename(fn, ".json") + puts "Opening #{fn}" a = CelesteMap.new(fn, fmt: :json) - a.write "bin/#{base}.bin" + outfn = "bin/#{base}.bin" + puts "Writing #{outfn}" + a.write outfn end diff --git a/report.json b/report.json new file mode 100644 index 0000000..329c4ac --- /dev/null +++ b/report.json @@ -0,0 +1,138 @@ +{ + "report": [ + "player", + "introCar", + "lamp", + "wire", + "introCrusher", + "jumpThru", + "flutterbird", + "bird", + "npc", + "bridge", + "hahaha", + "spikesUp", + "spikesLeft", + "goldenBerry", + "zipMover", + "spikesRight", + "crumbleBlock", + "refill", + "spring", + "strawberry", + "spikesDown", + "lightbeam", + "cassette", + "cassetteBlock", + "fallingBlock", + "fakeWall", + "dashBlock", + "checkpoint", + "bonfire", + "coverupWall", + "memorial", + "memorialTextController", + "birdForsakenCityGem", + "towerviewer", + "blackGem", + "touchSwitch", + "switchGate", + "dreamBlock", + "hanginglamp", + "floatingDebris", + "foregroundDebris", + "darkChaser", + "invisibleBarrier", + "exitBlock", + "payphone", + "dreammirror", + "rotateSpinner", + "door", + "redBlocks", + "yellowBlocks", + "resortLantern", + "greenBlocks", + "spinner", + "cobweb", + "soundSource", + "trackSpinner", + "blockField", + "resortmirror", + "waterfall", + "lockBlock", + "sinkingPlatform", + "key", + "triggerSpikesLeft", + "triggerSpikesRight", + "triggerSpikesUp", + "movingPlatform", + "trapdoor", + "water", + "clutterDoor", + "oshirodoor", + "colorSwitch", + "clutterCabinet", + "picoconsole", + "clothesline", + "triggerSpikesDown", + "friendlyGhost", + "resortRoofEnding", + "killbox", + "cloud", + "booster", + "cliffside_flag", + "moveBlock", + "cliffflag", + "ridgeGate", + "whiteblock", + "gondola", + "templeGate", + "dashSwitchH", + "torch", + "swapBlock", + "templeMirror", + "dashSwitchV", + "templeMirrorPortal", + "seeker", + "templeCrackedBlock", + "seekerStatue", + "seekerBarrier", + "conditionBlock", + "theoCrystalPedestal", + "theoCrystal", + "templeEye", + "theoCrystalHoldingBarrier", + "templeBigEyeball", + "playerSeeker", + "crushBlock", + "bigWaterfall", + "infiniteStar", + "reflectionHeartStatue", + "bigSpinner", + "badelineBoost", + "tentacles", + "finalBoss", + "finalBossFallingBlock", + "finalBossMovingBlock", + "plateau", + "starJumpBlock", + "SummitBackgroundManager", + "wallSpringLeft", + "wallSpringRight", + "summitgem", + "bridgeFixed", + "summitcheckpoint", + "summitGemManager", + "summitcloud", + "heartGemDoor", + "wallBooster", + "fireBall", + "bounceBlock", + "iceBlock", + "coreModeToggle", + "fireBarrier", + "risingLava", + "sandwichLava", + "coreMessage" + ] +} \ No newline at end of file diff --git a/rom.rb b/rom.rb deleted file mode 100644 index 49abe0b..0000000 --- a/rom.rb +++ /dev/null @@ -1,151 +0,0 @@ -# General purpose binary reader - -# 8 16 32 64 -# C S L Q - unsigned -# c s l q - unsigned -# < LE -# > BE -# A - binary string -# z - null terminated -# H - hex string - -class ROM - def initialize str - @rom = str - @cur = 0 - @base = 0 - end - - def self.from_file fn - data = File.open(fn, "rb") { |io| io.read } - ROM.new data - end - - def set_base pos - @base = pos - end - - def seek pos - @cur = pos + @base - end - - def seek_rel pos - @cur += pos - end - - def tell - @cur - @base - end - - def read_str len - r = @rom[@cur..(@cur + len - 1)].unpack("A#{len}").first - @cur += len - r - end - - def read_str_varlen - len = read_varlen_le - r = @rom[@cur..(@cur + len - 1)].unpack("A#{len}").first - @cur += len - r - end - - def read_byte - r = @rom[@cur].ord - @cur += 1 - r - end - - def read_bool - read_byte == 1 - end - - def read_s8 - r = @rom[@cur].unpack("c").first - @cur += 1 - r - end - - def read_u16_le - r = @rom[@cur..(@cur + 3)].unpack('S<').first - @cur += 2 - r - end - - def read_s16_le - r = @rom[@cur..(@cur + 3)].unpack('s<').first - @cur += 2 - r - end - - def read_u16_be - r = @rom[@cur..(@cur + 3)].unpack('S>').first - @cur += 2 - r - end - - def read_u32_le - r = @rom[@cur..(@cur + 3)].unpack('L<').first - @cur += 4 - r - end - - def read_s32_le - r = @rom[@cur..(@cur + 3)].unpack('l<').first - @cur += 4 - r - end - - def read_u32_be - r = @rom[@cur..(@cur + 3)].unpack('L>').first - @cur += 4 - r - end - - def read_f32_le - r = @rom[@cur..(@cur + 3)].unpack('e').first - @cur += 4 - r - end - - def read_bin len - r = @rom[@cur..(@cur + len - 1)] - @cur += len - r - end - - def read_binswap len - togo = len - bin_data = "".b - while togo > 0 - r = @rom[@cur..(@cur + 3)].unpack('L>') - bin_data += r.pack('L<') - @cur += 4 - togo -= 4 - end - bin_data - end - - def read_varlen_le - val = 0 - r = read_byte - val = r & 0x7F - return val if r < 0x80 - r = read_byte << 7 - val + r - end - - def read_varlen_be - val = 0 - r = read_byte - val = r & 0x7F - return val if r < 0x80 - r = read_byte - val = (val << 8) + r - val - end - - def msg str - puts "%08X(%08X): %s" % [@cur, tell, str] - end -end