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.
- data/HISTORY.txt +110 -0
- data/IFMapper.gemspec +1 -1
- data/TODO.txt +6 -2
- data/lib/IFMapper/Connection.rb +2 -1
- data/lib/IFMapper/FXConnection.rb +36 -18
- data/lib/IFMapper/FXConnectionDialogBox.rb +10 -2
- data/lib/IFMapper/FXMap.rb +68 -28
- data/lib/IFMapper/FXMapFileDialog.rb +1 -1
- data/lib/IFMapper/FXMapperSettings.rb +15 -5
- data/lib/IFMapper/FXMapperWindow.rb +102 -8
- data/lib/IFMapper/FXRoom.rb +1 -3
- data/lib/IFMapper/FXRoomDialogBox.rb +52 -16
- data/lib/IFMapper/FXSection.rb +9 -0
- data/lib/IFMapper/IFMReader.rb +5 -3
- data/lib/IFMapper/InformReader.rb +803 -0
- data/lib/IFMapper/InformReaderOld.rb +778 -0
- data/lib/IFMapper/InformWriter.rb +349 -0
- data/lib/IFMapper/Map.rb +12 -0
- data/lib/IFMapper/PDFMapExporter.rb +35 -17
- data/lib/IFMapper/Room.rb +4 -1
- data/lib/IFMapper/Section.rb +17 -3
- data/lib/IFMapper/TADSReader.rb +832 -0
- data/lib/IFMapper/TADSWriter.rb +360 -0
- data/lib/IFMapper/TranscriptReader.rb +167 -49
- metadata +16 -11
data/lib/IFMapper/Room.rb
CHANGED
@@ -41,7 +41,10 @@ class Room
|
|
41
41
|
@darkness = vars.shift
|
42
42
|
@x = vars.shift
|
43
43
|
@y = vars.shift
|
44
|
-
|
44
|
+
if not vars.empty? and vars[0].kind_of?(String)
|
45
|
+
@desc = vars.shift
|
46
|
+
@desc.sub!(/\n/, ' ')
|
47
|
+
end
|
45
48
|
end
|
46
49
|
|
47
50
|
def marshal_dump
|
data/lib/IFMapper/Section.rb
CHANGED
@@ -118,6 +118,16 @@ class Section
|
|
118
118
|
# Create a new connection among two rooms thru their exits
|
119
119
|
#
|
120
120
|
def new_connection( roomA, exitA, roomB, exitB = nil )
|
121
|
+
|
122
|
+
# Verify rooms exist in section (ie. don't allow links across
|
123
|
+
# sections)
|
124
|
+
if not @rooms.include?(roomA)
|
125
|
+
raise ConnectionError, "Room #{roomA} not in section #{self}"
|
126
|
+
end
|
127
|
+
if roomB and not @rooms.include?(roomB)
|
128
|
+
raise ConnectionError, "Room #{roomB} not in section #{self}"
|
129
|
+
end
|
130
|
+
|
121
131
|
c = Connection.new( roomA, roomB )
|
122
132
|
_new_connection( c, roomA, exitA, roomB, exitB )
|
123
133
|
end
|
@@ -179,9 +189,9 @@ EOF
|
|
179
189
|
|
180
190
|
if roomA[exitA]
|
181
191
|
if not @connections.include?(roomA[exitA])
|
182
|
-
raise ConnectionError, "
|
192
|
+
raise ConnectionError, "roomA exit #{exitA} for #{roomA} filled but not in section"
|
183
193
|
end
|
184
|
-
raise ConnectionError, "
|
194
|
+
raise ConnectionError, "roomA exit #{exitA} for #{roomA} is filled"
|
185
195
|
end
|
186
196
|
|
187
197
|
roomA[exitA] = c
|
@@ -201,7 +211,7 @@ EOF
|
|
201
211
|
|
202
212
|
if roomB and roomB[exitB] and roomB[exitB] != c
|
203
213
|
roomA[exitA] = nil
|
204
|
-
raise ConnectionError, "
|
214
|
+
raise ConnectionError, "roomB exit #{exitB} for #{roomB} is filled"
|
205
215
|
end
|
206
216
|
roomB[exitB] = c if roomB
|
207
217
|
|
@@ -209,4 +219,8 @@ EOF
|
|
209
219
|
return c
|
210
220
|
end
|
211
221
|
|
222
|
+
def to_s
|
223
|
+
return @name
|
224
|
+
end
|
225
|
+
|
212
226
|
end
|
@@ -0,0 +1,832 @@
|
|
1
|
+
|
2
|
+
require "IFMapper/Map"
|
3
|
+
|
4
|
+
class FXMap; end
|
5
|
+
|
6
|
+
#
|
7
|
+
# Class that allows creating a map from an TADS source file.
|
8
|
+
#
|
9
|
+
class TADSReader
|
10
|
+
|
11
|
+
class ParseError < StandardError; end
|
12
|
+
class MapError < StandardError; end
|
13
|
+
|
14
|
+
# Take a quoted TADS string and return a valid ASCII one, replacing
|
15
|
+
# TADS's special characters.
|
16
|
+
def self.unquote(text)
|
17
|
+
return '' unless text
|
18
|
+
text.gsub!(/\\'/, "'") # restore quotes
|
19
|
+
text.gsub!(/\\"/, '"') # restore quotes
|
20
|
+
text.gsub!(/<<.*>>/, '') # remove embedded functions
|
21
|
+
text.gsub!(/<\/?q>/, '"') # Change quotes
|
22
|
+
return text
|
23
|
+
end
|
24
|
+
|
25
|
+
# Temporary classes used to store inform room information
|
26
|
+
class TADSObject
|
27
|
+
attr_reader :name
|
28
|
+
attr_accessor :tag, :location, :enterable
|
29
|
+
|
30
|
+
def name=(x)
|
31
|
+
@name = TADSReader::unquote(x)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
"#@name tag:#@tag"
|
36
|
+
end
|
37
|
+
|
38
|
+
def method_missing(*a)
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(location)
|
42
|
+
if location
|
43
|
+
@location = Array[*location]
|
44
|
+
else
|
45
|
+
@location = []
|
46
|
+
end
|
47
|
+
@enterable = []
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class TADSDoor
|
52
|
+
attr_accessor :location, :locked, :connector, :tag
|
53
|
+
def method_missing(*x)
|
54
|
+
end
|
55
|
+
def initialize
|
56
|
+
@location = []
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class TADSRoom
|
61
|
+
attr_reader :name
|
62
|
+
attr_accessor :exits, :tag, :light, :desc
|
63
|
+
def to_s
|
64
|
+
"#@name tag:#@tag"
|
65
|
+
end
|
66
|
+
def name=(x)
|
67
|
+
@name = TADSReader::unquote(x)
|
68
|
+
end
|
69
|
+
def method_missing(*x)
|
70
|
+
end
|
71
|
+
def initialize
|
72
|
+
@desc = ''
|
73
|
+
@light = true
|
74
|
+
@exits = Array.new(12, nil)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# TADS 3 directional properties need to be spelled fully
|
79
|
+
DIRECTIONS = {
|
80
|
+
'north' => 0,
|
81
|
+
'northeast' => 1,
|
82
|
+
'east' => 2,
|
83
|
+
'southeast' => 3,
|
84
|
+
'south' => 4,
|
85
|
+
'southwest' => 5,
|
86
|
+
'west' => 6,
|
87
|
+
'northwest' => 7,
|
88
|
+
'up' => 8,
|
89
|
+
'down' => 9,
|
90
|
+
'in' => 10,
|
91
|
+
'out' => 11
|
92
|
+
}
|
93
|
+
|
94
|
+
FUNCTION = /^\[ (\w+);/
|
95
|
+
|
96
|
+
GO_OBJ = /\b(#{DIRECTIONS.keys.join('|').gsub(/_to/, '_obj')})\s*:/i
|
97
|
+
|
98
|
+
# SINGLE QUOTED STRING
|
99
|
+
SQ = '((?:\\\'|[^\'])+)'
|
100
|
+
|
101
|
+
# DOUBLE QUOTED STRING
|
102
|
+
DQ = '((?:\\\"|[^"])+)'
|
103
|
+
|
104
|
+
# Equal sign (TADS supports C or Pascal-like =)
|
105
|
+
EQ = '\s*(?:=|\:=)\s*'
|
106
|
+
|
107
|
+
# Direction list in order of positioning preference.
|
108
|
+
DIRLIST = [ 0, 4, 2, 6, 1, 3, 5, 7 ]
|
109
|
+
|
110
|
+
DIR_TO = /(?:^|\s+)(#{DIRECTIONS.keys.join('|')})#{EQ}/
|
111
|
+
DIR = /(?:^|\s+)(#{DIRECTIONS.keys.join('|')})#{EQ}(\w+)/
|
112
|
+
ENTER_DIR = //i # needed?
|
113
|
+
|
114
|
+
OBJLOCATION = /(?:^|\s+)(?:(?:destination|location)#{EQ}(\w+)|@(\w+)|locationList#{EQ}\[([\w\s,]+)\])/
|
115
|
+
OBJNAME = /(?:^|\s+)name#{EQ}'#{SQ}'/
|
116
|
+
CONNECTOR = /(?:^|\s+)room\d+\s*#{EQ}\s*(\w+)/
|
117
|
+
ROOMNAME = /(?:^|\s+)roomName#{EQ}'#{SQ}'/
|
118
|
+
DESCRIPTION = /(?:^|\s+)desc#{EQ}(?:\s*$|\s+"#{DQ}?("?))/
|
119
|
+
|
120
|
+
attr_reader :map
|
121
|
+
|
122
|
+
@@debug = nil
|
123
|
+
def debug(*x)
|
124
|
+
return unless @@debug
|
125
|
+
$stdout.puts x
|
126
|
+
$stdout.flush
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# Main parsing loop. We basically parse the file twice to
|
131
|
+
# solve dependencies. Yes, this is inefficient, but the alternative
|
132
|
+
# was to build a full parser that understands forward dependencies.
|
133
|
+
#
|
134
|
+
def parse(file)
|
135
|
+
# We start map at 0, 0
|
136
|
+
@x, @y = [0, 0]
|
137
|
+
@room = nil
|
138
|
+
|
139
|
+
if @map.kind_of?(FXMap)
|
140
|
+
@map.options['Edit on Creation'] = false
|
141
|
+
@map.window.hide
|
142
|
+
end
|
143
|
+
@map.section = 0
|
144
|
+
|
145
|
+
@parsing = nil
|
146
|
+
@last_section = 0
|
147
|
+
@ignore_first_section = true
|
148
|
+
@room_idx = 0
|
149
|
+
line_number = 0
|
150
|
+
|
151
|
+
debug "...Parse... #{file.path}"
|
152
|
+
while not file.eof?
|
153
|
+
@line = ''
|
154
|
+
while not file.eof? and @line == ''
|
155
|
+
@line << file.readline()
|
156
|
+
@line.sub!( /^\s*\/\/.*$/, '')
|
157
|
+
line_number += 1
|
158
|
+
end
|
159
|
+
# todo: Remove multi-line comments
|
160
|
+
# Remove comments at end of line
|
161
|
+
@line.sub!( /\s+\/\/[^"]*$/, '')
|
162
|
+
# Remove starting spaces (if any)
|
163
|
+
@line.sub! /^\s+/, ''
|
164
|
+
# Replace \n with simple space
|
165
|
+
@line.gsub! /\n/, ' '
|
166
|
+
next if @line == ''
|
167
|
+
full_line = @line.dup
|
168
|
+
begin
|
169
|
+
parse_line
|
170
|
+
rescue ParseError => e
|
171
|
+
$stderr.puts
|
172
|
+
$stderr.puts "#{e} at #{file.path}, line #{line_number}:"
|
173
|
+
$stderr.puts ">>>> #{full_line};"
|
174
|
+
$stderr.puts
|
175
|
+
end
|
176
|
+
end
|
177
|
+
debug "...End Parse..."
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
CLASS = /^class\s+(\w+)\s*:\s*([\w,\s]+)/i
|
182
|
+
DOOR = /(?:^|\s+)door_to(?:\s+([^,;]*)|$)/i
|
183
|
+
INCLUDE = /^#include\s+[<"]([^">]+)[">]/
|
184
|
+
|
185
|
+
STD_LIB = [
|
186
|
+
'adv3.h',
|
187
|
+
'en_us.h',
|
188
|
+
]
|
189
|
+
|
190
|
+
|
191
|
+
def new_room
|
192
|
+
# We assume we are a room (albeit we could be an obj)
|
193
|
+
@room = TADSRoom.new
|
194
|
+
@room.tag = @tag
|
195
|
+
@room.name = @name
|
196
|
+
@room.desc = @desc
|
197
|
+
@tags[@tag] = @room
|
198
|
+
@rooms << @room
|
199
|
+
end
|
200
|
+
|
201
|
+
def new_obj(loc = nil)
|
202
|
+
debug "+++ OBJECT #@name"
|
203
|
+
@obj = TADSObject.new(loc)
|
204
|
+
@obj.tag = @tag
|
205
|
+
@obj.name = @name
|
206
|
+
@tags[@tag] = @obj
|
207
|
+
@objects << @obj
|
208
|
+
end
|
209
|
+
|
210
|
+
|
211
|
+
def find_file(file)
|
212
|
+
return file if File.exists?(file)
|
213
|
+
@include_dirs.each { |d|
|
214
|
+
[ "#{d}/#{file}",
|
215
|
+
"#{d}/#{file}.h",
|
216
|
+
"#{d}/#{file}.inf", ].each { |full|
|
217
|
+
return full if File.exists?(full)
|
218
|
+
}
|
219
|
+
}
|
220
|
+
return nil
|
221
|
+
end
|
222
|
+
|
223
|
+
def get_parent_classes(clist)
|
224
|
+
ret = []
|
225
|
+
clist.each { |c|
|
226
|
+
c.strip!
|
227
|
+
if @classes[c]
|
228
|
+
ret += get_parent_classes(@classes[c])
|
229
|
+
else
|
230
|
+
ret << c
|
231
|
+
end
|
232
|
+
}
|
233
|
+
return ret
|
234
|
+
end
|
235
|
+
|
236
|
+
#
|
237
|
+
# Parse a line of file
|
238
|
+
#
|
239
|
+
def parse_line
|
240
|
+
if @line =~ INCLUDE
|
241
|
+
name = $1
|
242
|
+
unless STD_LIB.include?(name)
|
243
|
+
file = find_file(name)
|
244
|
+
if file
|
245
|
+
File.open(file, 'r') { |f| parse(f) }
|
246
|
+
else
|
247
|
+
raise ParseError, "Include file #{name} not found"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
if @line =~ CLASS
|
253
|
+
@clas = $1
|
254
|
+
inherits = $2
|
255
|
+
debug "CLASS: #@clas : #{inherits}"
|
256
|
+
@classes[@clas] = inherits.split(/\s*,\s*/)
|
257
|
+
@tag = @name = nil
|
258
|
+
return
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
|
263
|
+
#
|
264
|
+
# Handle room description
|
265
|
+
#
|
266
|
+
if @in_desc == 1 and @line =~ /\s*"#{DQ}("\s*)?/
|
267
|
+
# check for possible start description
|
268
|
+
@room.desc << $1
|
269
|
+
if $2
|
270
|
+
@in_desc = nil
|
271
|
+
else
|
272
|
+
@in_desc = 2
|
273
|
+
end
|
274
|
+
return
|
275
|
+
end
|
276
|
+
|
277
|
+
if @in_desc == 2
|
278
|
+
@line =~ /\s*#{DQ}?("\s*)?/
|
279
|
+
@room.desc << $1 if @room and $1
|
280
|
+
@in_desc = nil if $2
|
281
|
+
return
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
# TADS3 definition is like:
|
286
|
+
# [+] [TAG:] Classes ['adjective nouns'] 'Name' @location
|
287
|
+
plus = '\s*([\+]+\s*)?'
|
288
|
+
tag = '(?:(\w+)\s*:)?\s*'
|
289
|
+
cls = '([\w\s\-\>,]+)'
|
290
|
+
noms = "(?:\'#{SQ}\'\s+)?"
|
291
|
+
name = "(?:\'#{SQ}\')?"
|
292
|
+
loc = '\s+(?:@(\w+))?'
|
293
|
+
re = /^#{plus}#{tag}#{cls}#{noms}#{name}#{loc}/
|
294
|
+
if @line =~ re
|
295
|
+
|
296
|
+
prev = $1
|
297
|
+
@name = [$5, $4]
|
298
|
+
@tag = $2
|
299
|
+
@clas = $3 # can be several classes
|
300
|
+
@in_desc = false
|
301
|
+
|
302
|
+
|
303
|
+
loc = $6
|
304
|
+
if prev and @room
|
305
|
+
loc = @room.tag
|
306
|
+
end
|
307
|
+
|
308
|
+
## For user classes, flatten inheritance
|
309
|
+
clist = @clas.split(/(?:\s*,\s*|\s)/)
|
310
|
+
flist = get_parent_classes(clist)
|
311
|
+
@clas = flist.join(' ')
|
312
|
+
|
313
|
+
if @clas =~ /\b(Outdoor|Dark)?Room\b/
|
314
|
+
@obj = nil
|
315
|
+
@name = @name[1] || @name[0] || @tag
|
316
|
+
debug "+++ ROOM #@name TAG: #@tag"
|
317
|
+
@tag = @name if not @tag
|
318
|
+
@desc = ''
|
319
|
+
new_room
|
320
|
+
@room.light = false if $1 == 'Dark'
|
321
|
+
@in_desc = 1
|
322
|
+
elsif @clas =~ /\bFakeConnector\b/
|
323
|
+
@functions << @tag
|
324
|
+
elsif @clas =~ /\bRoomConnector\b/
|
325
|
+
debug "+++ CONNECTOR TAG: #@tag"
|
326
|
+
@desc = ''
|
327
|
+
@obj = TADSDoor.new
|
328
|
+
@obj.connector = true
|
329
|
+
@obj.tag = @tag
|
330
|
+
@tags[@tag] = @obj
|
331
|
+
@doors << @obj
|
332
|
+
elsif @clas =~ /\b(?:(?:Hidden|Secret)?Door|ThroughPassage|Stairway(?:Up|Down))\b/
|
333
|
+
@desc = ''
|
334
|
+
@name = @name[0] || @name[1]
|
335
|
+
@tag = @name if not @tag
|
336
|
+
debug "+++ DOOR #@tag"
|
337
|
+
if @clas =~ /\s+->\s*(\w+)/
|
338
|
+
debug "\tmatching side: #{@tag} -> #$1"
|
339
|
+
# this is the other side of the door... find matching side
|
340
|
+
@obj = @tags[$1]
|
341
|
+
if not @obj
|
342
|
+
@obj = TADSDoor.new
|
343
|
+
@obj.tag = @tag
|
344
|
+
@tags[$1] = @obj
|
345
|
+
end
|
346
|
+
@tags[@tag] = @obj
|
347
|
+
@obj.location << loc if loc
|
348
|
+
else
|
349
|
+
@obj = @tags[@tag] || TADSDoor.new
|
350
|
+
@obj.locked = true if @clas =~ /\bLockable(WithKey)?\b/
|
351
|
+
@obj.tag = @tag
|
352
|
+
@obj.location = [loc] if loc
|
353
|
+
@tags[@tag] = @obj
|
354
|
+
@doors << @obj
|
355
|
+
end
|
356
|
+
elsif @clas =~ /\b(?:Thing|Food|Person)\b/
|
357
|
+
@obj = nil
|
358
|
+
@desc = ''
|
359
|
+
@name = @name[0] || @name[1]
|
360
|
+
@tag = @name if not @tag
|
361
|
+
new_obj(loc) if @tag
|
362
|
+
else
|
363
|
+
@name = (@name[0] || @name[1] || @tag)
|
364
|
+
end
|
365
|
+
|
366
|
+
# debug <<"EOF"
|
367
|
+
# Name: #@name
|
368
|
+
# Classes : #@clas
|
369
|
+
# Tag : #@tag
|
370
|
+
# Location: #{loc} #{loc.class}
|
371
|
+
# EOF
|
372
|
+
|
373
|
+
|
374
|
+
end
|
375
|
+
|
376
|
+
if @obj
|
377
|
+
@obj.name = $1 if @line =~ OBJNAME
|
378
|
+
if @line =~ OBJLOCATION
|
379
|
+
if $1
|
380
|
+
locs = $1.split(/\s*,\s*/)
|
381
|
+
@obj.location += locs
|
382
|
+
end
|
383
|
+
end
|
384
|
+
@obj.location << $1 if @obj.connector and @line =~ CONNECTOR
|
385
|
+
end
|
386
|
+
|
387
|
+
if @room and @line =~ ROOMNAME
|
388
|
+
@room.name = $1
|
389
|
+
return
|
390
|
+
end
|
391
|
+
|
392
|
+
if @line =~ DESCRIPTION
|
393
|
+
@desc << $1 if @desc and $1
|
394
|
+
@in_desc = 2 if $2 == ''
|
395
|
+
end
|
396
|
+
|
397
|
+
dirs = @line.scan(DIR_TO) + @line.scan(DIR) # + @line.scan(ENTER_DIR)
|
398
|
+
if dirs.size > 0
|
399
|
+
dirs.each { |d, room|
|
400
|
+
next if room == 'nil'
|
401
|
+
dir = DIRECTIONS[d]
|
402
|
+
@room.exits[dir] = room
|
403
|
+
}
|
404
|
+
end
|
405
|
+
|
406
|
+
end
|
407
|
+
|
408
|
+
|
409
|
+
def shift_link(room, dir)
|
410
|
+
idx = dir + 1
|
411
|
+
idx = 0 if idx > 7
|
412
|
+
while idx != dir
|
413
|
+
break if not room[idx]
|
414
|
+
idx += 1
|
415
|
+
idx = 0 if idx > 7
|
416
|
+
end
|
417
|
+
if idx != dir
|
418
|
+
room[idx] = room[dir]
|
419
|
+
room[dir] = nil
|
420
|
+
# get position of other room
|
421
|
+
ox, oy = Room::DIR_TO_VECTOR[dir]
|
422
|
+
c = room[idx]
|
423
|
+
if c.roomA == room
|
424
|
+
b = c.roomB
|
425
|
+
else
|
426
|
+
b = c.roomA
|
427
|
+
end
|
428
|
+
x, y = [b.x, b.y]
|
429
|
+
x -= ox
|
430
|
+
y -= oy
|
431
|
+
dx, dy = Room::DIR_TO_VECTOR[idx]
|
432
|
+
@map.shift(x, y, -dx, -dy)
|
433
|
+
else
|
434
|
+
# raise "Warning. Cannot shift connection for #{room}."
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
|
439
|
+
def oneway_link?(a, b)
|
440
|
+
# First, check if room already has exit moving towards other room
|
441
|
+
a.exits.each_with_index { |e, idx|
|
442
|
+
next if not e or e.dir != Connection::AtoB
|
443
|
+
roomA = e.roomA
|
444
|
+
roomB = e.roomB
|
445
|
+
if roomA == a and roomB == b
|
446
|
+
return e
|
447
|
+
end
|
448
|
+
}
|
449
|
+
return nil
|
450
|
+
end
|
451
|
+
|
452
|
+
|
453
|
+
# Choose a direction to represent up/down/in/out.
|
454
|
+
def choose_dir(a, b, go = nil, exitB = nil)
|
455
|
+
if go
|
456
|
+
rgo = go % 2 == 0? go - 1 : go + 1
|
457
|
+
# First, check if room already has exit moving towards other room
|
458
|
+
a.exits.each_with_index { |e, idx|
|
459
|
+
next if not e
|
460
|
+
roomA = e.roomA
|
461
|
+
roomB = e.roomB
|
462
|
+
if roomA == a and roomB == b
|
463
|
+
e.exitAtext = go
|
464
|
+
return idx
|
465
|
+
elsif roomB == a and roomA == b
|
466
|
+
e.exitBtext = go
|
467
|
+
return idx
|
468
|
+
end
|
469
|
+
}
|
470
|
+
end
|
471
|
+
|
472
|
+
# We prefer directions that travel less... so we need to figure
|
473
|
+
# out where we start from...
|
474
|
+
if b
|
475
|
+
x = b.x
|
476
|
+
y = b.y
|
477
|
+
else
|
478
|
+
x = a.x
|
479
|
+
y = a.y
|
480
|
+
end
|
481
|
+
if exitB
|
482
|
+
dx, dy = Room::DIR_TO_VECTOR[exitB]
|
483
|
+
x += dx
|
484
|
+
y += dy
|
485
|
+
end
|
486
|
+
|
487
|
+
# No such luck... Pick a direction.
|
488
|
+
best = nil
|
489
|
+
bestscore = nil
|
490
|
+
|
491
|
+
DIRLIST.each { |dir|
|
492
|
+
# We prefer straight directions to diagonal ones
|
493
|
+
inc = dir % 2 == 1 ? 100 : 140
|
494
|
+
score = 1000
|
495
|
+
# We prefer directions where both that dir and the opposite side
|
496
|
+
# are empty.
|
497
|
+
if (not a[dir]) or a[dir].stub?
|
498
|
+
score += inc
|
499
|
+
score += 4 if a[dir] #attaching to stubs is better
|
500
|
+
end
|
501
|
+
# rdir = (dir + 4) % 8
|
502
|
+
# score += 1 unless a[rdir]
|
503
|
+
|
504
|
+
# Measure distance for that exit, we prefer shorter
|
505
|
+
# paths
|
506
|
+
dx, dy = Room::DIR_TO_VECTOR[dir]
|
507
|
+
dx = (a.x + dx) - x
|
508
|
+
dy = (a.y + dy) - y
|
509
|
+
d = dx * dx + dy * dy
|
510
|
+
score -= d
|
511
|
+
next if bestscore and score <= bestscore
|
512
|
+
bestscore = score
|
513
|
+
best = dir
|
514
|
+
}
|
515
|
+
|
516
|
+
if not bestscore
|
517
|
+
raise "No free exit for choose_dir"
|
518
|
+
end
|
519
|
+
|
520
|
+
return best
|
521
|
+
end
|
522
|
+
|
523
|
+
def make_room(from, to, x, y, dx = 1, dy = 0 )
|
524
|
+
elem = @tags[to.tag]
|
525
|
+
if elem.kind_of?(TADSRoom)
|
526
|
+
if not @map.free?(x, y)
|
527
|
+
@map.shift(x, y, dx, dy)
|
528
|
+
end
|
529
|
+
room = @map.new_room(x, y)
|
530
|
+
room.name = to.name
|
531
|
+
desc = to.desc
|
532
|
+
desc.gsub!(/[\t\n]/, ' ')
|
533
|
+
desc.squeeze!(' ')
|
534
|
+
room.desc = TADSReader::unquote(desc)
|
535
|
+
room.darkness = !to.light
|
536
|
+
@tags[to.tag] = room
|
537
|
+
return [room, Connection::FREE]
|
538
|
+
elsif elem.kind_of?(TADSDoor)
|
539
|
+
if elem.locked
|
540
|
+
type = Connection::LOCKED_DOOR
|
541
|
+
else
|
542
|
+
type = Connection::CLOSED_DOOR
|
543
|
+
end
|
544
|
+
|
545
|
+
@rooms.each { |o|
|
546
|
+
next if @tags[o.tag] == from
|
547
|
+
o.exits.each { |e|
|
548
|
+
next unless e
|
549
|
+
if @tags[e] == elem
|
550
|
+
res = make_room( o, o, x, y, dx, dy )
|
551
|
+
return [ res[0], type ]
|
552
|
+
end
|
553
|
+
}
|
554
|
+
}
|
555
|
+
|
556
|
+
# Okay, connecting room is missing. Check door's locations property
|
557
|
+
elem.location.each { |tag|
|
558
|
+
next if @tags[tag] == from
|
559
|
+
@rooms.each { |o|
|
560
|
+
next if o.tag != tag
|
561
|
+
res = make_room( o, o, x, y, dx, dy )
|
562
|
+
return [ res[0], type ]
|
563
|
+
}
|
564
|
+
}
|
565
|
+
|
566
|
+
#raise "error: no room with door #{to.name} #{elem.name}"
|
567
|
+
return [nil, nil]
|
568
|
+
else
|
569
|
+
return [elem, Connection::FREE]
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
def create_room(r, x, y, dx = 1, dy = 0)
|
574
|
+
from, = make_room(r, r, x, y)
|
575
|
+
debug "CREATE ROOM #{r.name} TAG:#{r.tag} SET FROM TO: #{from}"
|
576
|
+
|
577
|
+
r.exits.each_with_index { |e, exit|
|
578
|
+
next unless e
|
579
|
+
next if e == 'nothing' or e == '0'
|
580
|
+
debug "#{r.name} EXIT:#{exit} points to #{e}"
|
581
|
+
|
582
|
+
to = @tags[e]
|
583
|
+
if not to
|
584
|
+
next if @functions.include?(e)
|
585
|
+
raise "Room #{e} #{e.class} not found." if not to
|
586
|
+
end
|
587
|
+
|
588
|
+
go = c = nil
|
589
|
+
|
590
|
+
dir = exit
|
591
|
+
type = 0
|
592
|
+
|
593
|
+
# # If exit leads to an enterable object, find out where does that
|
594
|
+
# # enterable object lead to.
|
595
|
+
# if to.kind_of?(TADSObject)
|
596
|
+
# rooms = to.enterable
|
597
|
+
# rooms.each { |room|
|
598
|
+
# next if room == r
|
599
|
+
# to = @tags[room]
|
600
|
+
# break
|
601
|
+
# }
|
602
|
+
# # Skip it if we are still an object. This means we are just
|
603
|
+
# # a container, like the phone booth in the Fate game demo.
|
604
|
+
# next if to.kind_of?(TADSObject)
|
605
|
+
# end
|
606
|
+
|
607
|
+
if to.kind_of?(TADSRoom) or to.kind_of?(TADSDoor)
|
608
|
+
if dir > 7
|
609
|
+
# choose a dir for up/down/in/out
|
610
|
+
go = dir - 7
|
611
|
+
dir = choose_dir(from, nil, go)
|
612
|
+
end
|
613
|
+
|
614
|
+
dx, dy = Room::DIR_TO_VECTOR[dir]
|
615
|
+
x = from.x + dx
|
616
|
+
y = from.y + dy
|
617
|
+
debug "#{exit} CREATE TO #{from} -> #{to.tag}"
|
618
|
+
to, type = make_room(from, to, x, y, dx, dy)
|
619
|
+
next if not to
|
620
|
+
end
|
621
|
+
|
622
|
+
if exit > 7
|
623
|
+
# choose a dir for up/down/in/out
|
624
|
+
go = exit - 7
|
625
|
+
p "GO: #{go} choose_dir"
|
626
|
+
dir = choose_dir(from, to, go)
|
627
|
+
end
|
628
|
+
|
629
|
+
b = @rooms.find { |r2| r2.tag == e }
|
630
|
+
odir = nil
|
631
|
+
odir = b.exits.rindex(r.tag) if b
|
632
|
+
odir = (dir + 4) % 8 if not odir or odir > 7
|
633
|
+
|
634
|
+
if from[dir]
|
635
|
+
c = from[dir]
|
636
|
+
if to.exits.rindex(c) and c.roomB == from
|
637
|
+
debug "LINK TRAVELLED BOTH"
|
638
|
+
c.dir = Connection::BOTH
|
639
|
+
c.exitBtext = go if go
|
640
|
+
next
|
641
|
+
else
|
642
|
+
debug "#{exit} FROM #{from}->#{to} BLOCKED DIR: #{dir}"
|
643
|
+
shift_link(from, dir)
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
# Check we don't have a connection already
|
648
|
+
if to[odir]
|
649
|
+
c = to[odir]
|
650
|
+
debug "#{from} #{dir} -> #{to} dir:#{odir} filled. Swap..."
|
651
|
+
|
652
|
+
# We need to change odir to something else
|
653
|
+
rgo = 0
|
654
|
+
if go
|
655
|
+
rgo = go % 2 == 0? go - 1 : go + 1
|
656
|
+
end
|
657
|
+
|
658
|
+
# First, check if we have a dangling one-way link going to->from
|
659
|
+
# If we do, we use it.
|
660
|
+
c = oneway_link?(from, to)
|
661
|
+
if not c
|
662
|
+
odir = choose_dir(to, from, rgo, dir)
|
663
|
+
debug "Swapped to #{odir}"
|
664
|
+
else
|
665
|
+
debug "FOUND LINK #{c} -- filling it."
|
666
|
+
idx = from.exits.index(c)
|
667
|
+
from[idx] = nil
|
668
|
+
from[dir] = c
|
669
|
+
c.dir = Connection::BOTH
|
670
|
+
c.exitBtext = go if go
|
671
|
+
end
|
672
|
+
else
|
673
|
+
debug "to[odir] empty."
|
674
|
+
# First, check if we have a dangling one-way link going to->from
|
675
|
+
# If we do, we use it.
|
676
|
+
c = oneway_link?(to, from)
|
677
|
+
if c
|
678
|
+
debug "FOUND LINK #{c} -- filling it."
|
679
|
+
idx = from.exits.index(c)
|
680
|
+
from[idx] = nil
|
681
|
+
from[dir] = c
|
682
|
+
c.dir = Connection::BOTH
|
683
|
+
c.exitBtext = go if go
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
if not c
|
688
|
+
debug "NEW LINK #{from} #{dir} to #{to} #{odir}"
|
689
|
+
begin
|
690
|
+
c = @map.new_connection(from, dir, to, odir)
|
691
|
+
c.exitAtext = go if go
|
692
|
+
c.dir = Connection::AtoB
|
693
|
+
c.type = type
|
694
|
+
rescue Section::ConnectionError
|
695
|
+
end
|
696
|
+
end
|
697
|
+
}
|
698
|
+
|
699
|
+
return r
|
700
|
+
end
|
701
|
+
|
702
|
+
#
|
703
|
+
# Create all the stuff we found
|
704
|
+
#
|
705
|
+
def create
|
706
|
+
@rooms.each { |r| create_room(r, 0, 0) }
|
707
|
+
@rooms = []
|
708
|
+
|
709
|
+
# Add objects to rooms
|
710
|
+
@objects.each { |obj|
|
711
|
+
obj.location.each { |loc|
|
712
|
+
r = @tags[loc]
|
713
|
+
next unless r and r.kind_of?(Room)
|
714
|
+
r.objects << obj.name + "\n"
|
715
|
+
}
|
716
|
+
}
|
717
|
+
end
|
718
|
+
|
719
|
+
|
720
|
+
if RUBY_PLATFORM =~ /win/
|
721
|
+
SEP = ';'
|
722
|
+
else
|
723
|
+
SEP = ':'
|
724
|
+
end
|
725
|
+
|
726
|
+
#
|
727
|
+
# Bring up the TADS properties window, to allow user to change
|
728
|
+
# settings
|
729
|
+
#
|
730
|
+
def properties
|
731
|
+
decor = DECOR_TITLE|DECOR_BORDER
|
732
|
+
|
733
|
+
dlg = FXDialogBox.new( @map.window.parent, "TADS Settings", decor )
|
734
|
+
mainFrame = FXVerticalFrame.new(dlg,
|
735
|
+
FRAME_SUNKEN|FRAME_THICK|
|
736
|
+
LAYOUT_FILL_X|LAYOUT_FILL_Y)
|
737
|
+
|
738
|
+
frame = FXHorizontalFrame.new(mainFrame, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
|
739
|
+
|
740
|
+
FXLabel.new(frame, "Include Dirs: ", nil, 0, LAYOUT_FILL_X)
|
741
|
+
inc = FXTextField.new(frame, 80, nil, 0, LAYOUT_FILL_ROW)
|
742
|
+
inc.text = @include_dirs.join(SEP)
|
743
|
+
|
744
|
+
buttons = FXHorizontalFrame.new(mainFrame,
|
745
|
+
LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|
|
746
|
+
PACK_UNIFORM_WIDTH)
|
747
|
+
# Accept
|
748
|
+
FXButton.new(buttons, "&Accept", nil, dlg, FXDialogBox::ID_ACCEPT,
|
749
|
+
FRAME_RAISED|FRAME_THICK|LAYOUT_RIGHT|LAYOUT_CENTER_Y)
|
750
|
+
|
751
|
+
# Cancel
|
752
|
+
FXButton.new(buttons, "&Cancel", nil, dlg, FXDialogBox::ID_CANCEL,
|
753
|
+
FRAME_RAISED|FRAME_THICK|LAYOUT_RIGHT|LAYOUT_CENTER_Y)
|
754
|
+
if dlg.execute != 0
|
755
|
+
@include_dirs = inc.text.split(SEP)
|
756
|
+
return true
|
757
|
+
end
|
758
|
+
return false
|
759
|
+
end
|
760
|
+
|
761
|
+
|
762
|
+
|
763
|
+
def set_include_dirs
|
764
|
+
# Try to find t3make(.exe) in path.
|
765
|
+
paths = ENV['PATH'].split(SEP)
|
766
|
+
paths.each { |p|
|
767
|
+
next if not File.directory?(p)
|
768
|
+
Dir.foreach(p) { |x|
|
769
|
+
if x =~ /^t3make(.exe)?$/i
|
770
|
+
@include_dirs << p
|
771
|
+
@include_dirs << p + "/include"
|
772
|
+
@include_dirs << p + "/lib"
|
773
|
+
@include_dirs << p + "/contrib"
|
774
|
+
break
|
775
|
+
end
|
776
|
+
}
|
777
|
+
}
|
778
|
+
end
|
779
|
+
|
780
|
+
def initialize(file, map = Map.new('TADS Map'))
|
781
|
+
debug "Initialize"
|
782
|
+
@classes = { 'Object' => {} }
|
783
|
+
@tags = {}
|
784
|
+
@map = map
|
785
|
+
@objects = []
|
786
|
+
@doors = []
|
787
|
+
@functions = []
|
788
|
+
@rooms = []
|
789
|
+
|
790
|
+
@include_dirs = [File.dirname(file)]
|
791
|
+
set_include_dirs
|
792
|
+
|
793
|
+
|
794
|
+
debug "Get properties"
|
795
|
+
if @map.kind_of?(FXMap)
|
796
|
+
return unless properties
|
797
|
+
end
|
798
|
+
|
799
|
+
debug "Start parsing #{file}"
|
800
|
+
File.open(file) { |f|
|
801
|
+
parse(f)
|
802
|
+
}
|
803
|
+
debug "Done parsing #{file}"
|
804
|
+
puts "Rooms: #{@rooms.size}"
|
805
|
+
puts "Doors: #{@doors.size}"
|
806
|
+
puts "Objects: #{@objects.size}"
|
807
|
+
|
808
|
+
create
|
809
|
+
debug "Done creating #{file}"
|
810
|
+
|
811
|
+
if @map.kind_of?(FXMap)
|
812
|
+
@map.filename = file.sub(/\.t$/i, '.map')
|
813
|
+
@map.navigation = true
|
814
|
+
@map.options['Location Description'] = true
|
815
|
+
@map.window.show
|
816
|
+
end
|
817
|
+
@objects = nil
|
818
|
+
@tags = nil # save some memory by clearing the tag list
|
819
|
+
@rooms = nil # and room list
|
820
|
+
end
|
821
|
+
end
|
822
|
+
|
823
|
+
|
824
|
+
if $0 == __FILE__
|
825
|
+
p "Opening file '#{ARGV[0]}'"
|
826
|
+
BEGIN {
|
827
|
+
$LOAD_PATH << 'C:\Windows\Escritorio\IFMapper\lib'
|
828
|
+
}
|
829
|
+
|
830
|
+
require "IFMapper/Map"
|
831
|
+
TADSReader.new(ARGV[0])
|
832
|
+
end
|