Class: IFMReader

Inherits:
Object
  • Object
show all
Defined in:
lib/IFMapper/IFMReader.rb

Overview

Class that allows importing an IFM map file.

Defined Under Namespace

Classes: MapError, ParseError

Constant Summary collapse

DIRECTIONS =
{ 
  'north' => 0,
  'n' => 0,
  'northeast' => 1,
  'ne' => 1,
  'east' => 2,
  'e' => 2,
  'southeast' => 3,
  'se' => 3,
  'south' => 4,
  's' => 4,
  'southwest' => 5,
  'sw' => 5,
  'west' => 6,
  'w' => 6,
  'northwest' => 7,
  'nw' => 7,
}
GO =
{
  'd'    => 2,
  'down' => 2,
  'up'   => 1,
  'u'    => 1,
  'i'    => 3,
  'in'   => 3,
  'o'    => 4,
  'out'  => 4,
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file, map = Map.new('IFM Imported Map')) ⇒ IFMReader

Okay, check all min/max locations in all sections and then do the following:

a) Adjust map's width/height
b) Shift all rooms so that no rooms are in negative locations


548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# File 'lib/IFMapper/IFMReader.rb', line 548

def initialize(file, map = Map.new('IFM Imported Map'))
  @tags   = {}
  @map    = map
  @rooms  = []
  @resolve_tags = false

  # --------------- first pass
  File.open(file) { |f| 
    parse(f)
  }

  # --------------- second pass
  @map.fit
  @resolve_tags = true
  File.open(file) { |f|
    parse(f)
  }

  @map.section = 0
  if @map.kind_of?(FXMap)
    @map.filename   = file.sub(/\.ifm$/i, '.map')
    @map.navigation = true
    @map.window.show
  end
  @tags = {}   # save some memory by clearing the tag list
  @rooms = nil # and room list
end

Instance Attribute Details

#mapObject (readonly)

Returns the value of attribute map.



45
46
47
# File 'lib/IFMapper/IFMReader.rb', line 45

def map
  @map
end

Instance Method Details

#get_itemObject

Get an item



190
191
192
193
194
195
196
197
# File 'lib/IFMapper/IFMReader.rb', line 190

def get_item
  tag, item = get_tag
  return @item if tag == 'last'
  if item and item.kind_of?(Room)
    raise ParseError, "Not an item tag <#{tag}:#{item}>"
  end
  return item
end

#get_roomObject

Get a room



178
179
180
181
182
183
184
185
# File 'lib/IFMapper/IFMReader.rb', line 178

def get_room
  tag, room = get_tag
  return @room if tag == 'last'
  if room and not room.kind_of?(Room)
    raise ParseError, "Not a room tag <#{tag}:#{room}>" 
  end
  return room
end

#get_stringObject

Get a quoted string from line



108
109
110
111
112
113
114
115
116
117
# File 'lib/IFMapper/IFMReader.rb', line 108

def get_string
  str = nil
  if @line =~ /^"(([^"\\]|\\")*)"/
    str = $1
    @line = @line[str.size+2..-1]
    # Change any quoted " to normal "
    str.gsub!( /\\"/, '"' )
  end
  return str
end

#get_tagObject

Get a tag



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/IFMapper/IFMReader.rb', line 159

def get_tag
  tag = get_token
  if tag == 'it'
    return [tag, (@item or @task or @room)]
  elsif tag == 'last' or tag == 'all' or tag == 'any'
    return [tag, nil]
  else
    if not @tags[tag]
	if @resolve_tags
 raise ParseError, "Tag <#{tag}> not known"
	end
    end
    return [tag, @tags[tag]]
  end
end

#get_taskObject

Get a task



202
203
204
205
206
207
208
209
# File 'lib/IFMapper/IFMReader.rb', line 202

def get_task
  tag, task = get_tag
  return @task if tag == 'last'
  if task and task.kind_of?(Room)
    raise ParseError, "Not a task tag <#{tag}:#{item}>"
  end
  return task
end

#get_tokenObject

Get a new token from line



122
123
124
125
126
# File 'lib/IFMapper/IFMReader.rb', line 122

def get_token
  return nil if not @line
  token, @line = @line.split(' ', 2)
  return token
end

#is_tag?Boolean

Return whether next token is a tag or a language keyword

Returns:

  • (Boolean)


140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/IFMapper/IFMReader.rb', line 140

def is_tag?
  token = next_token
  case token
  when nil, 
	'to', 'cmd', 'from', 'need', 'lose', 'lost', 'tag', 'all', 'except',
	'before', 'after', 'start', 'style', 'endstyle', 'follow', 'link', 
	'until', 'dir', 'start', 'get', 'drop', 'goto', 'give', 'given', 
	'exit', 'in', 'keep', 'any', 'safe', 'score', 'style', 'go', 
	'hidden', 'oneway', 'finish', 'length', 'nopath', 'note', 'leave', 
	'join', /^#/
    return false
  else
    return true
  end
end

#next_tokenObject

Look-ahead a token, without modifying line



131
132
133
134
135
# File 'lib/IFMapper/IFMReader.rb', line 131

def next_token
  return nil if not @line
  token,  = @line.split(' ', 2)
  return token
end

#parse(file) ⇒ Object

Main parsing loop. We basically parse the file twice to solve dependencies. Yes, this is inefficient, but the alternative was to build a full parser that understands forward dependencies.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/IFMapper/IFMReader.rb', line 52

def parse(file)
  # We start map at 1, 1
  @x, @y = [0, 0]
  @room  = @item = @task  = nil

  if @map.kind_of?(FXMap)
    @map.options['Edit on Creation'] = false
    @map.window.hide
  end
  @map.section = 0

  @last_section = 0
  @ignore_first_section = true
  @room_idx = 0
  line_number = 0

  while not file.eof?
    @line = ''
    while not file.eof? and @line !~ /;\s*(#.*)?$/
	@line << file.readline()
	@line.sub!( /^\s*#.*/, '')
	line_number += 1
    end
    @line.sub!( /;\s*#.*$/, '')
    @line.sub!( /^\s+/, '' )
    @line.gsub!( /;\s*$/, '' )
    @line.gsub!( /\n/, ' ' )
    next if @line == ''
    full_line = @line.dup
    # puts "#{line_number}:'#{@line}'"
    begin
	parse_line
    rescue ParseError => e
	$stderr.puts
	$stderr.puts "#{e} for pass #{@resolve_tags}, at line #{line_number}:"
	$stderr.puts ">>>> #{full_line};"
	$stderr.puts
    rescue => e
	$stderr.puts
	$stderr.puts "#{e} for pass #{@resolve_tags}, at line #{line_number}:"
	$stderr.puts ">>>> #{full_line};"
	$stderr.puts e.backtrace if not e.kind_of?(MapError)
    end
  end
end

#parse_lineObject

Parse a line of file



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
# File 'lib/IFMapper/IFMReader.rb', line 214

def parse_line
  return if parse_variable

  roomname = tagname = nil
  @task = @item = roomA = roomB =  
    from = one_way = nolink = go = nil
  styles = []
  links = []
  dir = []


  roomA = @room  # add item to this room

  while @line and token = get_token
    case token
    when 'map'
	section = get_string
	# Once we start a new section, we rest room and
	# current position.
	@room = nil
	@x = @y = 1
	# dont' add a section for first 'map' keyword
	if @ignore_first_section
 @ignore_first_section = false
 @map.sections[0].name = section
 next
	end
	if @resolve_tags
 @map.section  = @last_section + 1
 @last_section = @map.section
	else
 @map.new_section
 @map.sections[-1].name = section
	end
    when 'title'
	@map.name = get_string
    when 'room'
	roomname = get_string
	@ignore_first_section = false
    when 'tag'
	tagname = get_token
    when 'dir'
	# if not roomname
	#  raise ParseError, 'dir directive found but not for a room'
	# end
	token = next_token
	while DIRECTIONS[token]
 get_token
 dir.push(DIRECTIONS[token])
 token = next_token

 if token =~ /^\d+$/
   get_token
   (token.to_i - 1).times { dir.push(dir[-1]) }
   token = next_token
 end
	end
    when 'nolink'
	if dir.empty?
 raise ParseError, 'nolink directive, but no dir directive before.'
	end
	nolink  = true
    when 'oneway'
	one_way = true
    when 'nopath'
    when 'safe'
    when 'exit'
	if not roomname
 raise ParseError, 'exit directive found but not for a room'
	end
	token = next_token
	while DIRECTIONS[token]
 get_token
 token = next_token
	end
    when 'from'
	if dir.empty?
 raise ParseError, "'from' token found but no dir specified before"
	end
	from = get_room
    when 'join'
	# Joins are links that are not drawn.  
	# Mainly to give the engine knowledge that two locations 
	# are interconnected
	get_room while is_tag?
	to = next_token
	if to == 'to'
 get_token
 get_room
	end
    when 'all'
    when 'lost'
    when 'except'
	get_item while is_tag?
    when 'length'
	get_token
    when 'until'
	get_task while is_tag?
    when 'link'
	if roomname
 while is_tag?
   links.push( get_room )
 end
	else
 roomA = get_room
 to = next_token
 if to == 'to'
   get_token
   roomB = get_room
 end
	end
    when 'goto'
	get_room
    when 'go'
	token = get_token
	go = GO[token]
	if not token
 raise ParseError, "Token <#{token}> is an unknown go direction."
	end
    when 'item'
	@item = get_string
	item_tag = get_token if not @item
    when 'in'
	roomA = get_room  # oh, well... this room
    when 'note'
	note = get_string
    when 'keep'
	token = next_token
	if token == 'with'
 get_token
 item_keep = get_item
	end
    when 'given'
    when 'give'
	give_items = [ get_item ]
	give_items.push(get_item) while is_tag?
    when 'start'
    when 'finish'
    when 'follow'
	task = get_task
    when 'need'
	need_items = [ get_item ]
	need_items.push(get_item) while is_tag?
    when 'after'
	after_tasks = [ get_item ]
	after_tasks.push(get_item) while is_tag?
    when 'lose'
	loose_items = [ get_item ]
	loose_items.push(get_item) while is_tag?
    when 'get'
	get_items = [ get_item ]
	get_items.push(get_item) while is_tag?
    when 'drop'
	drop_items = [ get_item ]
	drop_items.push(get_item) while is_tag?
    when 'hidden'
    when 'leave'
	leave_items = [ get_item ]
	leave_items.push(get_item) while is_tag?
    when 'before'
	task = get_task
    when 'cmd'
	token = next_token
	if token == 'to'
 get_token
 cmd = get_string
	elsif token == 'from'
 get_token
 cmd = get_string
	elsif token == 'none'
 get_token
	else
 cmd = get_string
 num = next_token
 get_token if num =~ /\d+/
	end
    when 'score'
	score = get_token.to_i
    when 'length'
	length = get_token.to_i
    when 'task'
	@task = get_string
	task_tag = get_token if not @task
    when 'style'
	styles.push(get_token) while is_tag?
    when 'endstyle'
	get_token while is_tag?
    when '', /^#/
	get_token while @line
    else
	raise ParseError, "Token <#{token.inspect}> not understood"
    end
  end

  # If a direction, move that way.
  if dir.size > 0
    # If from keyword present, move from that room on.
    if from
	roomA = from
	# 'from' can also connect stuff not in current section... 
	# so we check we are in the right section.
	@map.sections.each_with_index { |p, idx|
 if p.rooms.include?(roomA)
   @map.fit
   @map.section = idx
   break
 end
	}
    end
    # Okay, we start from roomA... and follow each dir
    if roomA
	@x = roomA.x
	@y = roomA.y
    end
    dir.each { |d|
	x, y = Room::DIR_TO_VECTOR[d]
	@x += x
	@y += y
    }
  end

  # Create new room
  if roomname
    if @resolve_tags
	# Room is already created.  Find it.
	roomB = @rooms[@room_idx]
	@room_idx += 1
    else
	# Verify there's no room in that location yet
	section = @map.sections[@map.section]
	section.rooms.each { |r|
 if r.x == @x and r.y == @y
   err  = "Section #{@map.section+1} already has location #{r} at #{@x}, #{@y}.\n"
   err << "Cannot create '#{roomname}'"
   raise MapError, err
 end
	}

	# Remember original room for connection
	roomB          = @map.new_room( @x, @y )
	roomB.selected = false
	roomB.name     = roomname
	@rooms.push( roomB )
    end

    # Make roomB the current room
    @room = roomB
  end

  if @item and roomA and @resolve_tags
    roomA.objects << @item + "\n"
  end

  if @task and @room and @resolve_tags
    @room.tasks << @task + "\n"
  end

  # Add a link between rooms
  if roomA and roomB and not nolink and @resolve_tags
    # Establish new simple connection
    dirB = (dir[-1] + 4) % 8 if dir.size > 1

    if dir.size > 1
	dirB = [ @x - roomB.x, @y - roomB.y ]
	if dirB[0] == 0 and dirB[1] == 0
 dirB = (dir[-1] + 4) % 8
	else
 dirB = roomB.vector_to_dir( dirB[0], dirB[1] )
	end
    end

    # 'from' and 'link' keywords can also connect stuff not in
    # current section... so we check for that here
    @map.sections.each_with_index { |p, idx|
	if p.rooms.include?(roomA)
 @map.fit
 @map.section = idx
 break
	end
    }
    if not @map.sections[@map.section].rooms.include?(roomB)
	raise MapError, "Linking #{roomA} and #{roomB} which are in different sections"
    end

    begin
	c = map.new_connection( roomA, dir[0], roomB, dirB )
	c.dir = Connection::AtoB if one_way
	c.type = Connection::SPECIAL if styles.include?('special')
    
	if go
 c.exitAtext = go
 if go % 2 == 0
   c.exitBtext = go - 1
 else
   c.exitBtext = go + 1
 end
	end
    rescue => e
	puts e
    end
  end

  if links.size > 0 and @resolve_tags
    # Additional diagonal connection for room
    links.each { |x| 
	next if not x
	begin
 c = map.new_connection( @room, nil, x, nil )
	rescue => e
 puts e
	end
    }
  end

  if tagname # and not @resolve_tags
    if roomname
	@tags[tagname] = @room
    elsif @item
	@tags[tagname] = @item
    elsif @task
	@tags[tagname] = @task
    else
	# join/link tag
	@tags[tagname] = ''
    end
  end
end

#parse_variableObject

see if line is a variable line. if so, return true



101
102
103
# File 'lib/IFMapper/IFMReader.rb', line 101

def parse_variable
  @line =~ /^\s*[\w_\-\.]+\s*=.*$/
end