ifmapper 0.8.5 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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