ifmapper 0.8.5 → 0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ class FXMapFileDialog < FXFileDialog
8
8
  @@last_path = nil
9
9
 
10
10
  KNOWN_EXTENSIONS = [
11
- "Map Files (*.map,*.ifm)",
11
+ "Map Files (*.map,*.ifm,*.inf,*.t)",
12
12
  "All Files (*)",
13
13
  ]
14
14
 
@@ -34,7 +34,9 @@ class FXMapperSettings < Hash
34
34
  f = home + '/.ifmapper'
35
35
  self.replace( YAML.load_file(f) )
36
36
  rescue
37
- self.replace( {
37
+ end
38
+
39
+ defaults = {
38
40
  # Colors
39
41
  'BG Color' => 'dark grey',
40
42
  'Arrow Color' => 'black',
@@ -51,15 +53,23 @@ class FXMapperSettings < Hash
51
53
  'Create on Connection' => true,
52
54
  'Edit on Creation' => false,
53
55
  'Automatic Connection' => true,
54
-
56
+
57
+
55
58
  # Display options
56
59
  'Use Room Cursor' => false,
57
60
  'Paths as Curves' => true,
58
- 'Location Numbers' => true,
61
+
62
+ # Location options
63
+ 'Location Tasks' => true,
64
+ 'Location Description' => false,
65
+ 'Location Numbers' => true,
66
+
67
+ # Grid options
59
68
  'Grid Boxes' => true,
60
69
  'Grid Straight Connections' => true,
61
70
  'Grid Diagonal Connections' => false,
62
- } )
63
- end
71
+ }
72
+
73
+ self.replace( defaults.merge(self) )
64
74
  end
65
75
  end
@@ -40,7 +40,7 @@ require 'IFMapper/FXMapColorBox'
40
40
  class FXMapperWindow < FXMainWindow
41
41
 
42
42
  PROGRAM_NAME = "Interactive Fiction Mapper"
43
- VERSION = '0.8.5'
43
+ VERSION = '0.9'
44
44
  AUTHOR = "Gonzalo Garramuno"
45
45
  TITLE = "#{PROGRAM_NAME} v#{VERSION} - Written by #{AUTHOR}"
46
46
 
@@ -66,6 +66,17 @@ class FXMapperWindow < FXMainWindow
66
66
  return map
67
67
  end
68
68
 
69
+ def open_tads(file, map)
70
+ require 'IFMapper/TADSReader'
71
+ TADSReader.new(file, map)
72
+ return map
73
+ end
74
+
75
+ def open_inform(file, map)
76
+ require 'IFMapper/InformReader'
77
+ InformReader.new(file, map)
78
+ return map
79
+ end
69
80
 
70
81
  def start_automap_cb(sender, sel, ptr)
71
82
  map = current_map
@@ -116,6 +127,10 @@ class FXMapperWindow < FXMainWindow
116
127
  tmp = nil
117
128
  if file =~ /\.ifm$/i
118
129
  tmp = open_ifm(file, map)
130
+ elsif file =~ /\.inf$/i
131
+ tmp = open_inform(file, map)
132
+ elsif file =~ /\.t$/i
133
+ tmp = open_tads(file, map)
119
134
  else
120
135
  tmp = open_map(file)
121
136
  end
@@ -131,12 +146,11 @@ class FXMapperWindow < FXMainWindow
131
146
  end
132
147
 
133
148
  map.copy(tmp)
134
- map.fit
135
- map.create_pathmap
149
+ map.options.replace( @@default_options.merge(map.options) )
136
150
  map.verify_integrity
151
+ map.fit
137
152
  map.window.create
138
- map.update_title
139
- map.draw
153
+ map.modified = false
140
154
  update_section
141
155
  status "Loaded '#{file}'."
142
156
  end
@@ -260,6 +274,41 @@ class FXMapperWindow < FXMainWindow
260
274
  end
261
275
  end
262
276
 
277
+ def inform_export_cb(sender, sel, msg)
278
+ map = current_map
279
+ return unless map
280
+
281
+ require 'IFMapper/InformWriter'
282
+
283
+ d = FXMapFileDialog.new(self, "Save Map as Inform Files",
284
+ [
285
+ "Inform Source Code (*.inf)"
286
+ ])
287
+ if d.filename != ''
288
+ file = d.filename
289
+ file.sub!(/(-\d+)?\.inf/, '')
290
+ InformWriter.new(map, file)
291
+ end
292
+ end
293
+
294
+
295
+ def tads_export_cb(sender, sel, msg)
296
+ map = current_map
297
+ return unless map
298
+
299
+ require 'IFMapper/TADSWriter'
300
+
301
+ d = FXMapFileDialog.new(self, "Save Map as TADS Files",
302
+ [
303
+ "TADS Source Code (*.t)"
304
+ ])
305
+ if d.filename != ''
306
+ file = d.filename
307
+ file.sub!(/(-\d+)?\.t/, '')
308
+ TADSWriter.new(map, file)
309
+ end
310
+ end
311
+
263
312
 
264
313
  def pdf_export_cb(sender, sel, msg)
265
314
  map = current_map
@@ -353,7 +402,7 @@ class FXMapperWindow < FXMainWindow
353
402
 
354
403
  def self.cut_selected(map)
355
404
  FXMapperWindow::copy_selected(map)
356
- map.delete_selected
405
+ map.cut_selected
357
406
  end
358
407
 
359
408
  def self.paste_selected(map)
@@ -401,8 +450,18 @@ class FXMapperWindow < FXMainWindow
401
450
  exitB = c.roomB.exits.rindex(c)
402
451
  roomA = r_to_nr[c.roomA]
403
452
  roomB = r_to_nr[c.roomB]
404
- c = map.new_connection(roomA, exitA, roomB, exitB)
405
- c.selected = true
453
+ next if not roomA or not roomB
454
+ if not exitA
455
+ puts "WHOA! #{c} #{roomA} #{exitA} is empty"
456
+ next
457
+ end
458
+ begin
459
+ c = map.new_connection(roomA, exitA, roomB, exitB)
460
+ c.selected = true
461
+ rescue Section::ConnectionError => e
462
+ puts c
463
+ puts e
464
+ end
406
465
  }
407
466
  end
408
467
 
@@ -659,6 +718,13 @@ EOF
659
718
 
660
719
  cmd = FXMenuCommand.new(submenu, "&Export as IFM...\t\tExport map as an IFM map.", nil)
661
720
  cmd.connect(SEL_COMMAND, method(:ifm_export_cb))
721
+
722
+ cmd = FXMenuCommand.new(submenu, "&Export as Inform Source...\t\tExport map as an Inform source code file.", nil)
723
+ cmd.connect(SEL_COMMAND, method(:inform_export_cb))
724
+
725
+ cmd = FXMenuCommand.new(submenu, "&Export as TADS3 Source...\t\tExport map as a TADS3 source code file.", nil)
726
+ cmd.connect(SEL_COMMAND, method(:tads_export_cb))
727
+
662
728
  FXMenuCascade.new(filemenu, "Export", nil, submenu)
663
729
 
664
730
 
@@ -873,6 +939,34 @@ EOF
873
939
  s.check = map.options['Location Numbers'] if map
874
940
  }
875
941
 
942
+ cmd = FXMenuCheck.new(submenu, "Location Tasks\t\tShow Tasks in Location Edit Box.")
943
+ cmd.check = @@default_options['Location Tasks']
944
+ cmd.connect(SEL_COMMAND) { |s, m, e|
945
+ map = current_map
946
+ if map
947
+ map.options['Location Tasks'] = (s.check == true)
948
+ map.draw
949
+ end
950
+ }
951
+ cmd.connect(SEL_UPDATE) { |s, m, e|
952
+ map = current_map
953
+ s.check = map.options['Location Tasks'] if map
954
+ }
955
+
956
+ cmd = FXMenuCheck.new(submenu, "Location Description\t\tShow Description in Location Edit Box.")
957
+ cmd.check = @@default_options['Location Description']
958
+ cmd.connect(SEL_COMMAND) { |s, m, e|
959
+ map = current_map
960
+ if map
961
+ map.options['Location Description'] = (s.check == true)
962
+ map.draw
963
+ end
964
+ }
965
+ cmd.connect(SEL_UPDATE) { |s, m, e|
966
+ map = current_map
967
+ s.check = map.options['Location Description'] if map
968
+ }
969
+
876
970
  cmd = FXMenuCheck.new(submenu, "Boxes\t\tDraw dashed box guidelines.")
877
971
  cmd.check = @@default_options['Grid Boxes']
878
972
  cmd.connect(SEL_COMMAND) { |s, m, e|
@@ -50,7 +50,7 @@ class FXRoom < Room
50
50
  # the room data over to it.
51
51
  #
52
52
  def selected=(value)
53
- if value and @@win
53
+ if value and @@win and @@win.shown?
54
54
  @@win.copy_from(self)
55
55
  end
56
56
  @selected = value
@@ -65,7 +65,6 @@ class FXRoom < Room
65
65
  @@win.hide
66
66
  end
67
67
  win = FXRoomDialogBox.new(map, self, nil, true)
68
- # win.setFocus
69
68
  win.setFocus
70
69
  win.show
71
70
  win.copy_from(self)
@@ -83,7 +82,6 @@ class FXRoom < Room
83
82
  def update_properties(map)
84
83
  return if not @@win or not @@win.shown?
85
84
  @@win.map = map
86
- @@win.setFocus
87
85
  @@win.copy_from(self)
88
86
  end
89
87
 
@@ -13,39 +13,59 @@ class FXRoomDialogBox < FXDialogBox
13
13
  @room.objects.gsub!(/[\.,\t]+/, "\n")
14
14
  @room.tasks = @tasks.text
15
15
  @room.darkness = (@darkness.checkState == 1)
16
+ @room.desc = @desc.text
16
17
 
17
- ## we should do something like:
18
- ## @room.draw(dc)
19
- ## for faster updates
20
- @map.draw
18
+ @map.draw_room(@room)
21
19
  end
22
20
 
23
21
 
24
22
  def copy_from(room)
23
+ @room = room
25
24
  @name.text = room.name
26
25
  @darkness.checkState = room.darkness
27
26
  @objects.text = room.objects
28
27
  @tasks.text = room.tasks
28
+ @desc.text = room.desc
29
+
29
30
  # Select text for quick editing if it uses default location name
30
- @name.setCursorPos room.name.size # - 1 if room.name.size > 0
31
+ @name.setCursorPos room.name.size
31
32
  if room.name == 'New Location'
33
+ self.setFocus
32
34
  @name.selectAll
33
- end
34
- if self.shown?
35
35
  @name.setFocus
36
36
  end
37
- @room = room
38
37
  if @map.navigation
39
38
  @name.disable
40
39
  @darkness.disable
41
40
  @objects.disable
42
41
  @tasks.disable
42
+ @desc.disable
43
43
  else
44
44
  @name.enable
45
45
  @darkness.enable
46
46
  @objects.enable
47
47
  @tasks.enable
48
+ @desc.enable
49
+ end
50
+
51
+ if @map.options['Location Tasks']
52
+ @tasksFrame.show
53
+ else
54
+ @tasksFrame.hide
55
+ end
56
+ if @map.options['Location Description']
57
+ @descFrame.show
58
+ else
59
+ @descFrame.hide
48
60
  end
61
+
62
+ @desc.connect(SEL_CHANGED) { @room.desc = @desc.text }
63
+
64
+ # Yuck! Fox's packer is absolutely oblivious to hiding/showing of
65
+ # elements, so we need to force a dummy resize so the layout is
66
+ # recalculated. But FUCK! This completely fucks up the focus.
67
+ # Fox is a piece of ****. I **REALLY** need to go back to FLTK.
68
+ ## self.resize(self.defaultWidth, self.defaultHeight)
49
69
  end
50
70
 
51
71
  def initialize(map, room, event = nil, modal = nil)
@@ -74,24 +94,39 @@ class FXRoomDialogBox < FXDialogBox
74
94
  FXLabel.new(frame, "Location: ", nil, 0, LAYOUT_FILL_X)
75
95
  @name = FXTextField.new(frame, 40, nil, 0, LAYOUT_FILL_ROW)
76
96
 
77
- frame = FXVerticalFrame.new(mainFrame,
97
+
98
+ all = FXHorizontalFrame.new(mainFrame,
78
99
  LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y)
79
100
 
80
-
81
- frame2 = FXHorizontalFrame.new(frame, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
101
+ leftFrame = FXVerticalFrame.new(all,
102
+ LAYOUT_SIDE_TOP|LAYOUT_SIDE_LEFT|
103
+ LAYOUT_FILL_X|LAYOUT_FILL_Y)
104
+
105
+ frame2 = FXHorizontalFrame.new(leftFrame, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
82
106
  FXLabel.new(frame2, "Objects: ", nil, 0, LAYOUT_FILL_X)
83
107
  @darkness = FXCheckButton.new(frame2, "Darkness", nil, 0,
84
108
  ICON_BEFORE_TEXT|LAYOUT_CENTER_X|
85
109
  LAYOUT_SIDE_RIGHT)
86
110
 
87
- @objects = FXText.new(frame, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)
111
+ @objects = FXText.new(leftFrame, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)
88
112
  @objects.visibleRows = 8
113
+ @objects.visibleColumns = 40
89
114
 
90
- frame = FXVerticalFrame.new(mainFrame, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|
91
- LAYOUT_FILL_Y)
92
- FXLabel.new(frame, "Tasks: ", nil, 0, LAYOUT_FILL_X)
93
- @tasks = FXText.new(frame, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)
115
+ @tasksFrame = FXVerticalFrame.new(leftFrame, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|
116
+ LAYOUT_FILL_Y)
117
+ FXLabel.new(@tasksFrame, "Tasks: ", nil, 0, LAYOUT_FILL_X)
118
+ @tasks = FXText.new(@tasksFrame, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)
94
119
  @tasks.visibleRows = 8
120
+ @tasks.visibleColumns = 40
121
+
122
+ ######## Add description
123
+ @descFrame = FXVerticalFrame.new(all, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|
124
+ LAYOUT_SIDE_RIGHT|LAYOUT_FILL_Y)
125
+ FXLabel.new(@descFrame, "Description: ", nil, 0, LAYOUT_FILL_X)
126
+ @desc = FXText.new(@descFrame, nil, 0,
127
+ LAYOUT_FILL_X|LAYOUT_FILL_Y|TEXT_WORDWRAP)
128
+ @desc.visibleColumns = 70
129
+ @desc.visibleRows = 18
95
130
 
96
131
  if modal
97
132
  buttons = FXHorizontalFrame.new(mainFrame,
@@ -109,6 +144,7 @@ class FXRoomDialogBox < FXDialogBox
109
144
  @objects.connect(SEL_CHANGED) { copy_to()}
110
145
  @tasks.connect(SEL_CHANGED) { copy_to() }
111
146
  @darkness.connect(SEL_COMMAND) { copy_to() }
147
+ @desc.connect(SEL_CHANGED) { @room.desc = @desc.text }
112
148
  end
113
149
 
114
150
  @map = map
@@ -5,6 +5,15 @@ require 'IFMapper/Section'
5
5
 
6
6
  class FXSection < Section
7
7
  def new_connection( roomA, exitA, roomB, exitB = nil )
8
+ # Verify rooms exist in section (ie. don't allow links across
9
+ # sections)
10
+ if not @rooms.include?(roomA)
11
+ raise ConnectionError, "Room #{roomA} not in section #{self}"
12
+ end
13
+ if roomB and not @rooms.include?(roomB)
14
+ raise ConnectionError, "Room #{roomB} not in section #{self}"
15
+ end
16
+
8
17
  c = FXConnection.new( roomA, roomB )
9
18
  return _new_connection(c, roomA, exitA, roomB, exitB)
10
19
  end
@@ -550,17 +550,19 @@ class IFMReader
550
550
  @map = map
551
551
  @rooms = []
552
552
  @resolve_tags = false
553
- # puts '--------------- first pass'
553
+
554
+ # --------------- first pass
554
555
  File.open(file) { |f|
555
556
  parse(f)
556
557
  }
557
- # puts '--------------- second pass'
558
+
559
+ # --------------- second pass
558
560
  @map.fit
559
561
  @resolve_tags = true
560
562
  File.open(file) { |f|
561
563
  parse(f)
562
564
  }
563
- # puts '--------------- second pass done'
565
+
564
566
  @map.section = 0
565
567
  if @map.kind_of?(FXMap)
566
568
  @map.filename = file.sub(/\.ifm$/i, '.map')
@@ -0,0 +1,803 @@
1
+
2
+ require "IFMapper/Map"
3
+
4
+ class FXMap; end
5
+
6
+ #
7
+ # Class that allows creating a map from an Inform source file.
8
+ #
9
+ class InformReader
10
+
11
+ class ParseError < StandardError; end
12
+ class MapError < StandardError; end
13
+
14
+ # Take a quoted Inform string and return a valid ASCII one, replacing
15
+ # Inform's special characters.
16
+ def self.inform_unquote(text)
17
+ return '' unless text
18
+ text.gsub!(/\~/, '"')
19
+ text.gsub!(/\^/, "\n")
20
+ while text =~ /(@@(\d+))/
21
+ text.sub!($1, $2.to_i.chr)
22
+ end
23
+ return text
24
+ end
25
+
26
+ # Temporary classes used to store inform room information
27
+ class InformObject
28
+ attr_reader :name
29
+ attr_accessor :tag, :location, :enterable
30
+
31
+ def name=(x)
32
+ @name = InformReader::inform_unquote(x)
33
+ end
34
+
35
+ def to_s
36
+ "#@name tag:#@tag"
37
+ end
38
+
39
+ def method_missing(*a)
40
+ end
41
+
42
+ def initialize(location)
43
+ if location
44
+ @location = Array[*location]
45
+ else
46
+ @location = []
47
+ end
48
+ @enterable = []
49
+ end
50
+ end
51
+
52
+ class InformDoor
53
+ attr_accessor :location
54
+ attr_accessor :locked
55
+ attr_accessor :tag
56
+ def method_missing(*x)
57
+ end
58
+ def initialize
59
+ @location = []
60
+ end
61
+ end
62
+
63
+ class InformRoom
64
+ attr_reader :name
65
+ attr_accessor :exits, :tag, :light, :desc
66
+ def to_s
67
+ "#@name tag:#@tag"
68
+ end
69
+ def name=(x)
70
+ @name = InformReader::inform_unquote(x)
71
+ end
72
+ def method_missing(*x)
73
+ end
74
+ def initialize
75
+ @desc = ''
76
+ @exits = Array.new(12, nil)
77
+ end
78
+ end
79
+
80
+
81
+ DIRECTIONS = {
82
+ 'n_to' => 0,
83
+ 'ne_to' => 1,
84
+ 'e_to' => 2,
85
+ 'se_to' => 3,
86
+ 's_to' => 4,
87
+ 'sw_to' => 5,
88
+ 'w_to' => 6,
89
+ 'nw_to' => 7,
90
+ 'u_to' => 8,
91
+ 'd_to' => 9,
92
+ 'in_to' => 10,
93
+ 'out_to' => 11
94
+ }
95
+
96
+ FUNCTION = /^\[ (\w+);/
97
+
98
+ GO_OBJ = /\b(#{DIRECTIONS.keys.join('|').gsub(/_to/, '_obj')})\s*:/i
99
+
100
+
101
+ # Direction list in order of positioning preference.
102
+ DIRLIST = [ 0, 4, 2, 6, 1, 3, 5, 7 ]
103
+
104
+ DIR_TO = /(?:^|\s+)(#{DIRECTIONS.keys.join('|')})\s+/i
105
+
106
+ DIR = /(?:^|\s+)(#{DIRECTIONS.keys.join('|')})\s+(\w+)/i
107
+ ENTER_DIR = /(?:^|\s+)(#{DIRECTIONS.keys.join('|')})\s+\[;\s*<<\s*Enter\s+(\w+)\s*>>/i
108
+
109
+ attr_reader :map
110
+
111
+ @@debug = nil
112
+ def debug(*x)
113
+ return unless @@debug
114
+ $stdout.puts x
115
+ $stdout.flush
116
+ end
117
+
118
+ #
119
+ # Main parsing loop. We basically parse the file twice to
120
+ # solve dependencies. Yes, this is inefficient, but the alternative
121
+ # was to build a full parser that understands forward dependencies.
122
+ #
123
+ def parse(file)
124
+ # We start map at 0, 0
125
+ @x, @y = [0, 0]
126
+ @room = nil
127
+
128
+ if @map.kind_of?(FXMap)
129
+ @map.options['Edit on Creation'] = false
130
+ @map.window.hide
131
+ end
132
+ @map.section = 0
133
+
134
+ @parsing = nil
135
+ @last_section = 0
136
+ @ignore_first_section = true
137
+ @room_idx = 0
138
+ line_number = 0
139
+
140
+ debug "...Parse... #{file.path}"
141
+ while not file.eof?
142
+ @line = ''
143
+ while not file.eof? and @line == ''
144
+ @line << file.readline()
145
+ @line.sub!( /^\s*!.*$/, '')
146
+ line_number += 1
147
+ end
148
+ # Remove comments at end of line
149
+ @line.sub!( /\s+![^"]*$/, '')
150
+ # Remove starting spaces (if any)
151
+ @line.sub! /^\s+/, ''
152
+ # Replace \n with simple space
153
+ @line.gsub! /\n/, ' '
154
+ next if @line == ''
155
+ full_line = @line.dup
156
+ begin
157
+ parse_line
158
+ rescue ParseError => e
159
+ $stderr.puts
160
+ $stderr.puts "#{e} at #{file.path}, line #{line_number}:"
161
+ $stderr.puts ">>>> #{full_line};"
162
+ $stderr.puts
163
+ end
164
+ end
165
+ debug "...End Parse..."
166
+ end
167
+
168
+
169
+ CLASS = /^class\s+(\w+)/i
170
+ DOOR = /(?:^|\s+)door_to(?:\s+([^,;]*)|$)/i
171
+ INCLUDE = /^#?include\s+"([^"]+)"/i
172
+ PLAYER_TO = /\bplayerto\((\w+)/i
173
+ DESCRIPTION = /(?:^|\s+)description(?:\s*$|\s+"([^"]+)?("?))/i
174
+
175
+ STD_LIB = [
176
+ 'Parser',
177
+ 'VerbLib',
178
+ 'Grammar'
179
+ ]
180
+
181
+
182
+ def new_room
183
+ debug "+++ ROOM: #@name"
184
+
185
+ # We assume we are a room (albeit we could be an obj)
186
+ @room = InformRoom.new
187
+ @room.tag = @tag
188
+ @room.name = @name
189
+ @room.desc = @desc
190
+ @room.light = @classes[@clas][:light]
191
+ @tags[@tag] = @room
192
+ @rooms << @room
193
+ end
194
+
195
+ def new_obj(loc = nil)
196
+ debug "+++ OBJ #@name"
197
+ @obj = InformObject.new(loc)
198
+ @obj.tag = @tag
199
+ @obj.name = @name
200
+ @tags[@tag] = @obj
201
+ @objects << @obj
202
+ end
203
+
204
+
205
+ def find_file(file)
206
+ return file if File.exists?(file)
207
+ @include_dirs.each { |d|
208
+ [ "#{d}/#{file}",
209
+ "#{d}/#{file}.h",
210
+ "#{d}/#{file}.inf", ].each { |full|
211
+ return full if File.exists?(full)
212
+ }
213
+ }
214
+ return nil
215
+ end
216
+
217
+ #
218
+ # Parse a line of file
219
+ #
220
+ def parse_line
221
+ if @line =~ INCLUDE
222
+ name = $1
223
+ unless STD_LIB.include?(name)
224
+ file = find_file(name)
225
+ if file
226
+ File.open(file, 'r') { |f| parse(f) }
227
+ else
228
+ raise ParseError, "Include file #{name} not found"
229
+ end
230
+ end
231
+ end
232
+
233
+ if @line =~ CLASS
234
+ @clas = $1
235
+ debug "CLASS: #@clas"
236
+ if @classes.has_key?(@clas)
237
+ if @obj
238
+ @classes[@clas].each { |k, v| @obj.send("#{k}=", v) }
239
+ elsif @room
240
+ @classes[@clas].each { |k, v| @room.send("#{k}=", v) }
241
+ end
242
+ else
243
+ @classes[@clas] = {}
244
+ @tag = @name = nil
245
+ end
246
+ end
247
+
248
+ re = /^(#{@classes.keys.join('|')})((?:\s+->)+)?(\s+(\w+))?(\s+"([^"]+)")?(\s+(\w+))?/
249
+ if @line =~ re
250
+ @clas = $1
251
+ prev = $2
252
+ @tag = $4 || $6
253
+ @name = $6
254
+ @desc = ''
255
+ @in_desc = false
256
+
257
+ loc = $8
258
+ if prev and @room
259
+ loc = @room.tag
260
+ end
261
+
262
+ debug <<"EOF"
263
+ Name : #@name
264
+ Class : #@clas
265
+ Tag : #@tag
266
+ Location: #{loc} #{loc.class}
267
+ EOF
268
+
269
+ @obj = nil
270
+
271
+ c = @classes[@clas]
272
+ if c[:door]
273
+ debug "+++ DOOR"
274
+ @obj = InformDoor.new
275
+ @obj.tag = @tag
276
+ @tags[@tag] = @obj
277
+ @doors << @obj
278
+ elsif loc or c[:scenery] or c[:static]
279
+ debug "+++ OBJECT #{loc} #{c[:scenery]}, #{c[:static]}"
280
+ new_obj(loc) if @tag
281
+ else
282
+ @obj = @room = nil
283
+ new_room if @tag and @name and c.has_key?(:light)
284
+ end
285
+ @before = false
286
+ @go = false
287
+
288
+ end
289
+
290
+ if @in_desc
291
+ text = @line.scan( /\s*([^"]+)("\s*[,;])?/ )
292
+ text.each { |t, q|
293
+ @desc << t
294
+ @in_desc = nil if q
295
+ }
296
+ end
297
+
298
+ if @desc and @line =~ DESCRIPTION
299
+ @desc << $1 if $1
300
+ @in_desc = true unless $2
301
+ end
302
+
303
+
304
+ if @line =~ FUNCTION
305
+ @functions << $1
306
+ end
307
+
308
+ dirs = @line.scan(DIR_TO) + @line.scan(DIR) + @line.scan(ENTER_DIR)
309
+ if dirs.size > 0
310
+ new_room if not @room
311
+ dirs.each { |d, room|
312
+ dir = DIRECTIONS[d]
313
+ @room.exits[dir] = room
314
+ }
315
+ end
316
+
317
+ if @line =~ /\bbefore\b/i
318
+ @before = true
319
+ end
320
+
321
+ if @line =~ /\bgo\s*:/i and @room and @before
322
+ if @line =~ GO_OBJ
323
+ dir = DIRECTIONS[$1]
324
+ if @line =~ PLAYER_TO
325
+ @room.exits[dir] = $1
326
+ end
327
+ end
328
+ end
329
+
330
+ if @obj.kind_of?(InformObject) and @before
331
+ if @line =~ PLAYER_TO
332
+ @obj.enterable << $1
333
+ end
334
+ end
335
+
336
+ if @tag and @line =~ DOOR
337
+ door = InformDoor.new
338
+ door.location = $1.split(' ')
339
+ door.location += @obj.location if @obj
340
+ door.tag = @tag
341
+ @obj = door
342
+ @tags[@tag] = door
343
+ @doors << door
344
+ @objects.delete(@obj) if @obj and @obj.tag == @tag
345
+ end
346
+
347
+ if @line =~ /(?:^|\s+)has\s+([\w\s]+)[,;]/i
348
+ props = $1.split
349
+ props.each { |p|
350
+ if not @tag
351
+ if p[0,1] == '~'
352
+ @classes[@clas][p[1,-1].to_sym] = false
353
+ else
354
+ @classes[@clas][p.to_sym] = true
355
+ end
356
+ else
357
+ if p =~ /locked/ and @doors.size > 0
358
+ @doors[-1].locked = true
359
+ end
360
+ if p =~ /(static|scenery)/ and @obj
361
+ @objects.delete(@obj)
362
+ end
363
+ if p =~ /(\~)?light/
364
+ new_room if not @room
365
+ @room.light = ($1 != '~')
366
+ end
367
+ end
368
+ }
369
+ end
370
+
371
+ if @line =~ /\bfound_in\s+(.*)[,;]?/
372
+ if @obj
373
+ locs = $1.split
374
+ @obj.location = locs
375
+ debug "#{@obj} #{@obj.location} FOUND_IN: #{locs.join(' ')}"
376
+ end
377
+ end
378
+ end
379
+
380
+
381
+ def shift_link(room, dir)
382
+ idx = dir + 1
383
+ idx = 0 if idx > 7
384
+ while idx != dir
385
+ break if not room[idx]
386
+ idx += 1
387
+ idx = 0 if idx > 7
388
+ end
389
+ if idx != dir
390
+ room[idx] = room[dir]
391
+ room[dir] = nil
392
+ # get position of other room
393
+ ox, oy = Room::DIR_TO_VECTOR[dir]
394
+ c = room[idx]
395
+ if c.roomA == room
396
+ b = c.roomB
397
+ else
398
+ b = c.roomA
399
+ end
400
+ x, y = [b.x, b.y]
401
+ x -= ox
402
+ y -= oy
403
+ dx, dy = Room::DIR_TO_VECTOR[idx]
404
+ @map.shift(x, y, -dx, -dy)
405
+ else
406
+ # raise "Warning. Cannot shift connection for #{room}."
407
+ end
408
+ end
409
+
410
+
411
+ def oneway_link?(a, b)
412
+ # First, check if room already has exit moving towards other room
413
+ a.exits.each_with_index { |e, idx|
414
+ next if not e or e.dir != Connection::AtoB
415
+ roomA = e.roomA
416
+ roomB = e.roomB
417
+ if roomA == a and roomB == b
418
+ return e
419
+ end
420
+ }
421
+ return nil
422
+ end
423
+
424
+
425
+ # Choose a direction to represent up/down/in/out.
426
+ def choose_dir(a, b, go = nil, exitB = nil)
427
+ if go
428
+ rgo = go % 2 == 0? go - 1 : go + 1
429
+ # First, check if room already has exit moving towards other room
430
+ a.exits.each_with_index { |e, idx|
431
+ next if not e or e.stub?
432
+ roomA = e.roomA
433
+ roomB = e.roomB
434
+ if roomA == a and roomB == b
435
+ e.exitAtext = go
436
+ return idx
437
+ elsif roomB == a and roomA == b
438
+ e.exitBtext = go
439
+ return idx
440
+ end
441
+ }
442
+ end
443
+
444
+ # We prefer directions that travel less... so we need to figure
445
+ # out where we start from...
446
+ if b
447
+ x = b.x
448
+ y = b.y
449
+ else
450
+ x = a.x
451
+ y = a.y
452
+ end
453
+ if exitB
454
+ dx, dy = Room::DIR_TO_VECTOR[exitB]
455
+ x += dx
456
+ y += dy
457
+ end
458
+
459
+ # No such luck... Pick a direction.
460
+ best = nil
461
+ bestscore = nil
462
+
463
+ DIRLIST.each { |dir|
464
+ # We prefer straight directions to diagonal ones
465
+ inc = dir % 2 == 1 ? 100 : 140
466
+ score = 1000
467
+ # We prefer directions where both that dir and the opposite side
468
+ # are empty.
469
+ if (not a[dir]) or a[dir].stub?
470
+ score += inc
471
+ score += 4 if a[dir] #attaching to stubs is better
472
+ end
473
+ # rdir = (dir + 4) % 8
474
+ # score += 1 unless a[rdir]
475
+
476
+ # Measure distance for that exit, we prefer shorter
477
+ # paths
478
+ dx, dy = Room::DIR_TO_VECTOR[dir]
479
+ dx = (a.x + dx) - x
480
+ dy = (a.y + dy) - y
481
+ d = dx * dx + dy * dy
482
+ score -= d
483
+ next if bestscore and score <= bestscore
484
+ bestscore = score
485
+ best = dir
486
+ }
487
+
488
+ if not bestscore
489
+ raise "No free exit for choose_dir"
490
+ end
491
+
492
+ return best
493
+ end
494
+
495
+ def make_room(from, to, x, y, dx = 1, dy = 0 )
496
+ elem = @tags[to.tag]
497
+ if elem.kind_of?(InformRoom)
498
+ if not @map.free?(x, y)
499
+ @map.shift(x, y, dx, dy)
500
+ end
501
+ room = @map.new_room(x, y)
502
+ room.name = to.name
503
+ desc = to.desc
504
+ desc.gsub!(/[\t\n]/, ' ')
505
+ desc.squeeze!(' ')
506
+ room.desc = InformReader::inform_unquote(desc)
507
+ room.darkness = !to.light
508
+ @tags[to.tag] = room
509
+ return [room, Connection::FREE]
510
+ elsif elem.kind_of?(InformDoor)
511
+ if elem.locked
512
+ type = Connection::LOCKED_DOOR
513
+ else
514
+ type = Connection::CLOSED_DOOR
515
+ end
516
+
517
+ @rooms.each { |o|
518
+ next if @tags[o.tag] == from
519
+ o.exits.each { |e|
520
+ next unless e
521
+ if @tags[e] == elem
522
+ res = make_room( o, o, x, y, dx, dy )
523
+ return [ res[0], type ]
524
+ end
525
+ }
526
+ }
527
+
528
+ # Okay, connecting room is missing. Check door's locations property
529
+ elem.location.each { |tag|
530
+ next if @tags[tag] == from
531
+ @rooms.each { |o|
532
+ next if o.tag != tag
533
+ res = make_room( o, o, x, y, dx, dy )
534
+ return [ res[0], type ]
535
+ }
536
+ }
537
+
538
+ #raise "error: no room with door #{to.name} #{elem.name}"
539
+ return [nil, nil]
540
+ else
541
+ return [elem, Connection::FREE]
542
+ end
543
+ end
544
+
545
+ def create_room(r, x, y, dx = 1, dy = 0)
546
+ from, = make_room(r, r, x, y)
547
+ debug "CREATE ROOM #{r.name} SET FROM TO: #{from}"
548
+
549
+ r.exits.each_with_index { |e, exit|
550
+ next unless e
551
+ next if e == 'nothing' or e == '0'
552
+ debug "#{r.name} EXIT:#{exit} points to #{e}"
553
+
554
+ to = @tags[e]
555
+ if not to
556
+ next if @functions.include?(e)
557
+ raise "Room #{e} #{e.class} not found." if not to
558
+ end
559
+
560
+ go = c = nil
561
+
562
+ dir = exit
563
+ type = 0
564
+
565
+ # If exit leads to an enterable object, find out where does that
566
+ # enterable object lead to.
567
+ if to.kind_of?(InformObject)
568
+ rooms = to.enterable
569
+ rooms.each { |room|
570
+ next if room == r
571
+ to = @tags[room]
572
+ break
573
+ }
574
+ # Skip it if we are still an object. This means we are just
575
+ # a container, like the phone booth in the Fate game demo.
576
+ next if to.kind_of?(InformObject)
577
+ end
578
+
579
+ if to.kind_of?(InformRoom) or to.kind_of?(InformDoor)
580
+ if dir > 7
581
+ # choose a dir for up/down/in/out
582
+ go = dir - 7
583
+ dir = choose_dir(from, nil, go)
584
+ end
585
+
586
+ dx, dy = Room::DIR_TO_VECTOR[dir]
587
+ x = from.x + dx
588
+ y = from.y + dy
589
+ debug "#{exit} CREATE TO #{from} -> #{to.tag}"
590
+ to, type = make_room(from, to, x, y, dx, dy)
591
+ next if not to
592
+ end
593
+
594
+ if exit > 7
595
+ # choose a dir for up/down/in/out
596
+ go = exit - 7
597
+ dir = choose_dir(from, to, go)
598
+ end
599
+
600
+ b = @rooms.find { |r2| r2.tag == e }
601
+ odir = nil
602
+ odir = b.exits.rindex(r.tag) if b
603
+ odir = (dir + 4) % 8 if not odir or odir > 7
604
+
605
+ if from[dir]
606
+ c = from[dir]
607
+ if to[odir] == c and c.roomB == from
608
+ debug "LINK TRAVELLED BOTH"
609
+ c.dir = Connection::BOTH
610
+ c.exitBtext = go if go
611
+ next
612
+ else
613
+ debug "#{exit} FROM #{from}->#{to} BLOCKED DIR: #{dir}"
614
+ shift_link(from, dir)
615
+ end
616
+ end
617
+
618
+ # Check we don't have a connection already
619
+ if to[odir]
620
+ c = to[odir]
621
+ debug "#{from} #{dir} -> #{to} dir:#{odir} filled. Swap..."
622
+
623
+ # We need to change odir to something else
624
+ rgo = 0
625
+ if go
626
+ rgo = go % 2 == 0? go - 1 : go + 1
627
+ end
628
+
629
+ # First, check if we have a dangling one-way link going to->from
630
+ # If we do, we use it.
631
+ c = oneway_link?(from, to)
632
+ if not c
633
+ odir = choose_dir(to, from, rgo, dir)
634
+ debug "Swapped to #{odir}"
635
+ else
636
+ debug "FOUND LINK #{c} -- filling it."
637
+ idx = from.exits.index(c)
638
+ from[idx] = nil
639
+ from[dir] = c
640
+ c.dir = Connection::BOTH
641
+ c.exitBtext = go if go
642
+ end
643
+ else
644
+ debug "to[odir] empty."
645
+ # First, check if we have a dangling one-way link going to->from
646
+ # If we do, we use it.
647
+ c = oneway_link?(to, from)
648
+ if c
649
+ debug "FOUND LINK #{c} -- filling it."
650
+ idx = from.exits.index(c)
651
+ from[idx] = nil
652
+ from[dir] = c
653
+ c.dir = Connection::BOTH
654
+ c.exitBtext = go if go
655
+ end
656
+ end
657
+
658
+ if not c
659
+ debug "NEW LINK #{from} #{dir} to #{to} #{odir}"
660
+ begin
661
+ c = @map.new_connection(from, dir, to, odir)
662
+ c.exitAtext = go if go
663
+ c.dir = Connection::AtoB
664
+ c.type = type
665
+ rescue Section::ConnectionError
666
+ end
667
+ end
668
+ }
669
+
670
+ return r
671
+ end
672
+
673
+ #
674
+ # Create all the stuff we found
675
+ #
676
+ def create
677
+ @rooms.each { |r| create_room(r, 0, 0) }
678
+ @rooms = []
679
+
680
+ # Add objects to rooms
681
+ @objects.each { |obj|
682
+ obj.location.each { |loc|
683
+ r = @tags[loc]
684
+ next unless r and r.kind_of?(Room)
685
+ r.objects << obj.name + "\n"
686
+ }
687
+ }
688
+ end
689
+
690
+
691
+ if RUBY_PLATFORM =~ /win/
692
+ SEP = ';'
693
+ else
694
+ SEP = ':'
695
+ end
696
+
697
+ #
698
+ # Bring up the Inform properties window, to allow user to change
699
+ # settings
700
+ #
701
+ def properties
702
+ decor = DECOR_TITLE|DECOR_BORDER
703
+
704
+ dlg = FXDialogBox.new( @map.window.parent, "Inform Settings", decor )
705
+ mainFrame = FXVerticalFrame.new(dlg,
706
+ FRAME_SUNKEN|FRAME_THICK|
707
+ LAYOUT_FILL_X|LAYOUT_FILL_Y)
708
+
709
+ frame = FXHorizontalFrame.new(mainFrame, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
710
+
711
+ FXLabel.new(frame, "Include Dirs: ", nil, 0, LAYOUT_FILL_X)
712
+ inc = FXTextField.new(frame, 80, nil, 0, LAYOUT_FILL_ROW)
713
+ inc.text = @include_dirs.join(SEP)
714
+
715
+ buttons = FXHorizontalFrame.new(mainFrame,
716
+ LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|
717
+ PACK_UNIFORM_WIDTH)
718
+ # Accept
719
+ FXButton.new(buttons, "&Accept", nil, dlg, FXDialogBox::ID_ACCEPT,
720
+ FRAME_RAISED|FRAME_THICK|LAYOUT_RIGHT|LAYOUT_CENTER_Y)
721
+
722
+ # Cancel
723
+ FXButton.new(buttons, "&Cancel", nil, dlg, FXDialogBox::ID_CANCEL,
724
+ FRAME_RAISED|FRAME_THICK|LAYOUT_RIGHT|LAYOUT_CENTER_Y)
725
+ if dlg.execute != 0
726
+ @include_dirs = inc.text.split(SEP)
727
+ return true
728
+ end
729
+ return false
730
+ end
731
+
732
+
733
+
734
+ def set_include_dirs
735
+ # Try to find inform(.exe) in path.
736
+ paths = ENV['PATH'].split(SEP)
737
+ paths.each { |p|
738
+ next if not File.directory?(p)
739
+ Dir.foreach(p) { |x|
740
+ if x =~ /^inform(.exe)?$/i
741
+ @include_dirs << p
742
+ @include_dirs << p + "/Base"
743
+ @include_dirs << p + "/Contrib"
744
+ @include_dirs << p + "/../Contrib"
745
+ break
746
+ end
747
+ }
748
+ }
749
+ end
750
+
751
+ def initialize(file, map = Map.new('Inform Map'))
752
+ debug "Initialize"
753
+ @classes = { 'Object' => {} }
754
+ @tags = {}
755
+ @map = map
756
+ @objects = []
757
+ @doors = []
758
+ @functions = []
759
+ @rooms = []
760
+
761
+ @include_dirs = [File.dirname(file)]
762
+ set_include_dirs
763
+
764
+
765
+ debug "Get properties"
766
+ if @map.kind_of?(FXMap)
767
+ return unless properties
768
+ end
769
+
770
+ debug "Start parsing #{file}"
771
+ File.open(file) { |f|
772
+ parse(f)
773
+ }
774
+ debug "Done parsing #{file}"
775
+ puts "Rooms: #{@rooms.size}"
776
+ puts "Doors: #{@doors.size}"
777
+ puts "Objects: #{@objects.size}"
778
+
779
+ create
780
+ debug "Done creating #{file}"
781
+
782
+ if @map.kind_of?(FXMap)
783
+ @map.filename = file.sub(/\.inf$/i, '.map')
784
+ @map.navigation = true
785
+ @map.options['Location Description'] = true
786
+ @map.window.show
787
+ end
788
+ @objects = nil
789
+ @tags = nil # save some memory by clearing the tag list
790
+ @rooms = nil # and room list
791
+ end
792
+ end
793
+
794
+
795
+ if $0 == __FILE__
796
+ p "Opening file '#{ARGV[0]}'"
797
+ BEGIN {
798
+ $LOAD_PATH << 'C:\Windows\Escritorio\IFMapper\lib'
799
+ }
800
+
801
+ require "IFMapper/Map"
802
+ InformReader.new(ARGV[0])
803
+ end