=head1 NAME Crossfire - Crossfire maphandling =cut package Crossfire; our $VERSION = '0.1'; use strict; use base 'Exporter'; use Carp (); use Storable; use File::Spec; use List::Util qw(min max); #XXX: The map_* procedures scream for a map-object our @EXPORT = qw(read_pak read_arch %ARCH TILESIZE $TILE %FACE editor_archs arch_extents); our $LIB = $ENV{CROSSFIRE_LIBDIR} or Carp::croak "\$CROSSFIRE_LIBDIR must be set\n"; sub TILESIZE (){ 32 } our $CACHEDIR; our %ARCH; our %FACE; our $TILE; our %FIELD_MULTILINE = ( msg => "endmsg", lore => "endlore", ); # not used yet, maybe alphabetical is ok our @FIELD_ORDER = (qw(name name_pl)); sub MOVE_WALK (){ 0x1 } sub MOVE_FLY_LOW (){ 0x2 } sub MOVE_FLY_HIGH (){ 0x4 } sub MOVE_FLYING (){ 0x6 } sub MOVE_SWIM (){ 0x8 } sub MOVE_ALL (){ 0xf } sub normalize_arch($) { my ($ob) = @_; my $arch = $ARCH{$ob->{_name}} or (warn "$ob->{_name}: no such archetype", return $ob); delete $ob->{$_} for qw(can_knockback can_parry can_impale can_cut can_dam_armour can_apply); if ($arch->{type} == 22) { # map my %normalize = ( "enter_x" => "hp", "enter_y" => "sp", "width" => "x", "height" => "y", "reset_timeout" => "weight", "swap_time" => "value", "difficulty" => "level", "darkness" => "invisible", "fixed_resettime" => "stand_still", ); while (my ($k2, $k1) = each %normalize) { if (defined (my $v = delete $ob->{$k1})) { $ob->{$k2} = $v; } } } if (defined (my $v = delete $ob->{no_pass})) { $ob->{move_block} = $v ? MOVE_ALL : 0; } if (defined (my $v = delete $ob->{walk_on})) { $ob->{move_on} = $v ? $ob->{move_on} | MOVE_WALK : $ob->{move_on} & ~MOVE_WALK; } if (defined (my $v = delete $ob->{walk_off})) { $ob->{move_off} = $v ? $ob->{move_off} | MOVE_WALK : $ob->{move_off} & ~MOVE_WALK; } if (defined (my $v = delete $ob->{fly_on})) { $ob->{move_on} = $v ? $ob->{move_on} | MOVE_FLY_LOW : $ob->{move_on} & ~MOVE_FLY_LOW; } if (defined (my $v = delete $ob->{fly_off})) { $ob->{move_off} = $v ? $ob->{move_off} | MOVE_FLY_LOW : $ob->{move_off} & ~MOVE_FLY_LOW; } if (defined (my $v = delete $ob->{flying})) { $ob->{move_type} = $v ? $ob->{move_type} | MOVE_FLY_LOW : $ob->{move_type} & ~MOVE_FLY_LOW; } # if value matches archetype default, delete while (my ($k, $v) = each %$ob) { if (exists $arch->{$k} and $arch->{$k} eq $v) { next if $k eq "_name"; delete $ob->{$k}; } } $ob } sub read_pak($;$) { my ($path, $cache) = @_; eval { defined $cache && -M $cache < -M $path && Storable::retrieve $cache } or do { my %pak; open my $fh, "<:raw", $path or Carp::croak "$_[0]: $!"; while (<$fh>) { my ($type, $id, $len, $path) = split; $path =~ s/.*\///; read $fh, $pak{$path}, $len; } Storable::nstore \%pak, $cache if defined $cache; \%pak } } sub read_arch($;$) { my ($path, $cache) = @_; eval { defined $cache && -M $cache < -M $path && Storable::retrieve $cache } or do { my %arc; my ($more, $prev); open my $fh, "<:raw", $path or Carp::croak "$path: $!"; my $parse_block; $parse_block = sub { my %arc = @_; while (<$fh>) { s/\s+$//; if (/^end$/i) { last; } elsif (/^arch (\S+)$/) { push @{ $arc{inventory} }, normalize_arch $parse_block->(_name => $1); } elsif (/^lore$/) { while (<$fh>) { last if /^endlore\s*$/i; $arc{lore} .= $_; } } elsif (/^msg$/) { while (<$fh>) { last if /^endmsg\s*$/i; $arc{msg} .= $_; } } elsif (/^(\S+)\s*(.*)$/) { $arc{lc $1} = $2; } elsif (/^\s*($|#)/) { # } else { warn "$path: unparsable line '$_' in arch $arc{_name}"; } } \%arc }; while (<$fh>) { s/\s+$//; if (/^more$/i) { $more = $prev; } elsif (/^object (\S+)$/i) { my $name = $1; my $arc = $parse_block->(_name => $name); if ($more) { $more->{more} = $arc; } else { $arc{$name} = $arc; } $prev = $arc; $more = undef; } elsif (/^arch (\S+)$/i) { push @{ $arc{arch} }, normalize_arch $parse_block->(_name => $1); } elsif (/^\s*($|#)/) { # } else { warn "$path: unparseable top-level line '$_'"; } } undef $parse_block; # work around bug in perl not freeing $fh etc. Storable::nstore \%arc, $cache if defined $cache; \%arc } } # put all archs into a hash with editor_face as it's key # NOTE: the arrays in the hash values are references to # the archs from $ARCH sub editor_archs { my %paths; for (keys %ARCH) { my $arch = $ARCH{$_}; push @{$paths{$arch->{editor_folder}}}, \$arch; } \%paths } =item ($minx, $miny, $maxx, $maxy) = arch_extents $arch arch_extents determines the extents of the given arch's face(s), linked faces and single faces are handled here it returns (minx, miny, maxx, maxy) =cut sub arch_extents { my ($a) = @_; my $o = $ARCH{$a->{_name}} or return; my $face = $FACE{$a->{face} || $o->{face}} or (warn "no face data found for arch '$a->{_name}'"), return; if ($face->{w} > 1 || $face->{h} > 1) { # bigface return (0, 0, $face->{w} - 1, $face->{h} - 1); } elsif ($o->{more}) { # linked face my ($minx, $miny, $maxx, $maxy) = ($o->{x}, $o->{y}) x 2; for (; $o; $o = $o->{more}) { $minx = min $minx, $o->{x}; $miny = min $miny, $o->{y}; $maxx = max $maxx, $o->{x}; $maxy = max $maxy, $o->{y}; } return ($minx, $miny, $maxx, $maxy); } else { # single face return (0, 0, 0, 0); } } sub init($) { my ($cachedir) = @_; return if %ARCH; *ARCH = read_arch "$LIB/archetypes", "$cachedir/archetypes.pst"; } =item $type = arch_attr $arch Returns a hashref describing the object and its attributes. It can contain the following keys: name the name, suitable for display purposes ignore attr desc use section => [name => \%attr, name => \%attr] import =cut sub arch_attr($) { my ($arch) = @_; require Crossfire::Data; my $attr; if ($arch->{type} > 0) { $attr = $Crossfire::Data::ATTR{$arch->{type}+0}; } else { $attr = $Crossfire::Data::TYPE{Misc}; type: for (@Crossfire::Data::ATTR0) { my $req = $_->{required} or die "internal error: ATTR0 without 'required'"; while (my ($k, $v) = each %$req) { next type unless $arch->{$k} == $v; } $attr = $_; } } $attr || \%Crossfire::Data::DEFAULT_ATTR; } sub arch_edit_sections { # if (edit_type == IGUIConstants.TILE_EDIT_NONE) # edit_type = 0; # else if (edit_type != 0) { # // all flags from 'check_type' must be unset in this arch because they get recalculated now # edit_type &= ~check_type; # } # # } # if ((check_type & IGUIConstants.TILE_EDIT_MONSTER) != 0 && # getAttributeValue("alive", defarch) == 1 && # (getAttributeValue("monster", defarch) == 1 || # getAttributeValue("generator", defarch) == 1)) { # // Monster: monsters/npcs/generators # edit_type |= IGUIConstants.TILE_EDIT_MONSTER; # } # if ((check_type & IGUIConstants.TILE_EDIT_WALL) != 0 && # arch_type == 0 && getAttributeValue("no_pass", defarch) == 1) { # // Walls # edit_type |= IGUIConstants.TILE_EDIT_WALL; # } # if ((check_type & IGUIConstants.TILE_EDIT_CONNECTED) != 0 && # getAttributeValue("connected", defarch) != 0) { # // Connected Objects # edit_type |= IGUIConstants.TILE_EDIT_CONNECTED; # } # if ((check_type & IGUIConstants.TILE_EDIT_EXIT) != 0 && # arch_type == 66 || arch_type == 41 || arch_type == 95) { # // Exit: teleporter/exit/trapdoors # edit_type |= IGUIConstants.TILE_EDIT_EXIT; # } # if ((check_type & IGUIConstants.TILE_EDIT_TREASURE) != 0 && # getAttributeValue("no_pick", defarch) == 0 && (arch_type == 4 || # arch_type == 5 || arch_type == 36 || arch_type == 60 || # arch_type == 85 || arch_type == 111 || arch_type == 123 || # arch_type == 124 || arch_type == 130)) { # // Treasure: randomtreasure/money/gems/potions/spellbooks/scrolls # edit_type |= IGUIConstants.TILE_EDIT_TREASURE; # } # if ((check_type & IGUIConstants.TILE_EDIT_DOOR) != 0 && # arch_type == 20 || arch_type == 23 || arch_type == 26 || # arch_type == 91 || arch_type == 21 || arch_type == 24) { # // Door: door/special door/gates + keys # edit_type |= IGUIConstants.TILE_EDIT_DOOR; # } # if ((check_type & IGUIConstants.TILE_EDIT_EQUIP) != 0 && # getAttributeValue("no_pick", defarch) == 0 && ((arch_type >= 13 && # arch_type <= 16) || arch_type == 33 || arch_type == 34 || # arch_type == 35 || arch_type == 39 || arch_type == 70 || # arch_type == 87 || arch_type == 99 || arch_type == 100 || # arch_type == 104 || arch_type == 109 || arch_type == 113 || # arch_type == 122 || arch_type == 3)) { # // Equipment: weapons/armour/wands/rods # edit_type |= IGUIConstants.TILE_EDIT_EQUIP; # } # # return(edit_type); # # } $CACHEDIR ||= $ENV{HOME} ? "$ENV{HOME}/.crossfire" : File::Spec->tmpdir; init $CACHEDIR; =head1 AUTHOR Marc Lehmann http://home.schmorp.de/ Robin Redeker http://www.ta-sa.org/ =cut 1