require 'rexml/document' require 'matrix' doc = nil File::open("#{ARGV[0]}.xml") do |f| doc = REXML::Document.new(f.read) end # this code is a mess $events = {} $current_event_id = 0 def make_event_number(name) return $events[name] if $events.key?(name) puts "making event for #{name}" $events[name] = $current_event_id $current_event_id += 1 if /(?.+)_(?normal|secret)/ =~ name raise "level #{level_name} not known" unless $points.key?(level_name) point = $points[level_name] point.params[2] = $events[name] if type == 'normal' point.params[3] = $events[name] if type == 'secret' end $events[name] end class StringTable attr_reader :size attr_accessor :start_offset def initialize @strings = [] @offset_hash = {} @size = 0 @start_offset = 0 end def include?(str) @offset_hash.key? str end def push(str) return if include? str str = str.dup @strings << str @offset_hash[str] = @size @size += str.length + 1 end def offset(str) push(str) unless include?(str) @start_offset + @offset_hash[str] end def to_a @strings.dup end def to_s out = [] @strings.each {|str| out << str; out << "\0" } out.join '' end alias :<< :push alias :[] :offset end class Model attr_accessor :resource_key attr_accessor :name attr_accessor :lightmap_type attr_accessor :matrix def initialize(element=nil) return if element.nil? @resource_key = element.attribute('from').value @name = element.attribute('name').value lm = element.attribute('lightmap').value @lightmap_type = case lm when 'map' then 0 when 'mapobj' then 1 else 0xFFFFFFFF end # TODO; solve this glaring omission @matrix = Matrix.columns([[1,0,0], [0,1,0], [0,0,1], [0,0,0]]) end end class Exit attr_accessor :path attr_accessor :reverse def initialize(element) @path = element.attribute('to').value @reverse = false unless element.attribute('reverse').nil? @reverse = true end end end class Point attr_accessor :exits attr_accessor :type attr_accessor :params attr_accessor :position attr_reader :id def initialize(element) @exits = {} @type = '' @params = [0,0,0,0] @position = [0,0,0] position = element.attribute 'position' @position = position.value.split(',').collect { |v| v.to_f } type = element.attribute 'type' params = element.attribute 'params' @type = type.value unless type.nil? @params = params.value.split(',').collect { |v| v.to_i } unless params.nil? if @type == 'level' level = element.attribute('level').value.split('-').map(&:to_i) #normal_event = element.attribute('event') #secret_event = element.attribute('secret_event') #normal_event = normal_event.nil? ? 0xFFFF : normal_event.value.to_i #secret_event = secret_event.nil? ? 0xFFFF : secret_event.value.to_i # phased those out for now normal_event, secret_event = 0xFFFF, 0xFFFF @params = level + [normal_event, secret_event] model = Model.new model.resource_key = 'LV_M' model.name = 'LevelMarker' model.lightmap_type = 1 model.matrix = Matrix.columns([[1,0,0], [0,1,0], [0,0,1], @position]) $models['L%03d' % $point_id] = model puts "created marker model for this point" end element.each_element 'exit' do |e| @exits[e.attribute('direction').value] = Exit.new(e) end @id = $point_id $point_id += 1 end end class Segment attr_accessor :start_pos attr_accessor :end_pos attr_accessor :speed attr_accessor :anim attr_accessor :anim_speed attr_accessor :direction, :same_dir attr_reader :id # todo: actions def initialize(element) from = element.attribute('from').value to = element.attribute('to').value if $points.has_key? from @start_pos = $points[from].position puts "From: point #{from} #{@start_pos}" else @start_pos = from.split(',').collect { |v| v.to_f } puts "From: position #{from}" end if $points.has_key? to @end_pos = $points[to].position puts "To: point #{to} #{@end_pos}" else @end_pos = to.split(',').collect { |v| v.to_f } puts "To: position #{to}" end @speed = 4.5 speed = element.attribute 'speed' unless speed.nil? @speed = speed.value.to_f end @anim = 'run' @anim_speed = 2.0 anim = element.attribute 'animation' unless anim.nil? @anim = anim.value end anim_speed = element.attribute 'animation_speed' unless anim_speed.nil? @anim_speed = anim_speed.value.to_f end @direction = nil direction = element.attribute 'direction' unless direction.nil? val = direction.value if val == 'last' @direction = nil elsif val == 'up' @direction = 270 elsif val == 'right' @direction = 0 elsif val == 'left' @direction = 180 elsif val == 'down' @direction = 90 else @direction = val.to_f end else #p @start_pos #p @end_pos if @start_pos[0] == @end_pos[0] and @start_pos[2] == @end_pos[2] # special case @direction = nil else @direction = Math.atan2(@end_pos[2]-@start_pos[2], @end_pos[0]-@start_pos[0]) @direction *= 180 / Math::PI end end @same_dir = !(element.attribute('same_dir').nil?) @id = $segment_id $segment_id += 1 $segments << self end end class Path attr_accessor :segments attr_accessor :segment_ids attr_accessor :start_point attr_accessor :end_point attr_reader :id attr_accessor :materials attr_accessor :event def initialize(element) @start_point = element.attribute('start').value @end_point = element.attribute('end').value @segments = [] @segment_ids = [] element.each_element 'segment' do |e| @segments << Segment.new(e) @segment_ids << @segments.last.id end @materials = [] element.each_element 'material' do |m| @materials << m.get_text.value.strip end event_attrib = element.attribute('required') @event = 0xFFFFFFFF unless event_attrib.nil? @event = make_event_number(event_attrib.value) end @id = $path_id $path_id += 1 end end point_types = {} point_types[''] = 0 # default point_types['level'] = 1 anim_types = {} anim_types[''] = 1 # default open('player_anim_list.txt') do |f| f.each do |line| match = line.match /(\d+)=(.+)/ if match anim_types[match[2]] = match[1].to_i unless anim_types.include? match[2] end end end directions = {'left' => 0, 'right' => 1, 'up' => 2, 'down' => 3} $string_table = StringTable.new # initial: scene models $models = {} puts "[[[ Getting models ]]]" doc.each_element 'map/model' do |m| puts "Reading #{m.attribute 'key'}..." raise "key must be 4 characters long" unless m.attribute('key').value.length == 4 $models[m.attribute('key').value] = Model.new(m) end # first, collect every point $points = {} $point_id = 0 puts "[[[ Getting points ]]]" doc.each_element 'map/point' do |p| puts "Reading #{p.attribute 'name'}..." $points[p.attribute('name').value] = Point.new(p) end puts "[[[ Points done ]]]" # next, collect every path $paths = {} $path_id = 0 $segments = [] $segment_id = 0 puts "[[[ Getting paths ]]]" doc.each_element 'map/path' do |p| puts "Reading #{p.attribute 'name'}..." $paths[p.attribute('name').value] = Path.new(p) end puts "[[[ Paths done ]]]" puts "Statistics: #{$points.count} points, #{$paths.count} paths, #{$segments.count} segments" # now write the file File::open("#{ARGV[0]}.wm", "wb") do |f| f << 'NwWM' # calculate all this stuff header_size = 0x24 path_list_offs = header_size point_list_offs = path_list_offs + ($paths.count * 4) segment_list_offs = point_list_offs + ($points.count * 4) action_list_offs = segment_list_offs + ($segments.count * 4) data_offset = action_list_offs # + ($actions.count * 4) f << [path_list_offs, $paths.count].pack('NN') f << [point_list_offs, $points.count].pack('NN') f << [segment_list_offs, $segments.count].pack('NN') f << [action_list_offs, 0].pack('NN') # now work out the offsets for everything current_offset = data_offset $paths.each_value do |p| f << [current_offset].pack('N') current_offset += 0xC + (p.segments.count * 4) + (p.materials.count * 4) end $points.each_value do |p| f << [current_offset].pack('N') current_offset += 0x40 end $segments.each do |s| f << [current_offset].pack('N') current_offset += 0x2C end # nothing for actions yet! # write the string table $string_table.start_offset = current_offset # now write the data $paths.each_value do |p| f << [$points[p.start_point].id, $points[p.end_point].id].pack('NN') f << [p.event, p.segments.count, p.materials.count].pack('ncc') f << p.segment_ids.pack('N*') p.materials.each do |mat| $string_table << mat f << [$string_table.offset(mat)].pack('N') end end $points.each_value do |p| ['left', 'right', 'up', 'down'].each do |direction| if p.exits.include? direction e = p.exits[direction] f << [$paths[e.path].id].pack('N') f << [e.reverse ? 1 : 0].pack('Cxxx') else f << [-1,-1].pack('NN') end end f << [point_types[p.type]].pack('N') f << p.params.pack('N*') f << p.position.pack('g*') end $segments.each do |s| f << s.start_pos.pack('g*') f << s.end_pos.pack('g*') f << [s.speed].pack('g') f << [anim_types[s.anim]].pack('N') f << [s.anim_speed].pack('g') if s.direction.nil? f << [0, 1, 0].pack('nCC') else f << [((90.0 - s.direction) % 360) / (360.0 / 65536.0), 0, s.same_dir ? 1 : 0].pack('nCC') end f << [0].pack('N') end f << $string_table.to_s end File::open("#{ARGV[0]}.scene", "wb") do |f| f << 'MScn' f << [$models.count].pack('N') st = StringTable.new st.start_offset = 8 + (0x40 * $models.count) $models.each_pair do |key,m| f << key f << m.resource_key f << [m.lightmap_type, st[m.name]].pack('NN') f << m.matrix.to_a.flatten.pack('g*') end f << st.to_s end