# ========= Copyright © Valve Corporation, All rights reserved. ==== #!perl use strict; use Math::Trig; use Scalar::Util; # get the arguments if ( @ARGV < 1 ) { print( "\nUsage: export_entity_group.pl [output.nut] [GroupName]\n" ); exit; } my $filename = $ARGV[0]; $filename =~ /(.*[\\\/])\w*.vmf/; my $path = $1; print( "path: $path\n" ); my $outputname; my $groupname; if ( @ARGV > 1 ) { $outputname = $ARGV[1]; } else { # Auto-generate the output filename $outputname = $filename; $outputname =~ s/content(.*)maps[\\\/]instance(.*)\.vmf/game$1scripts\\vscripts$2_group\.nut/; } if ( @ARGV > 2 ) { $groupname = $ARGV[2]; } else { # Auto-generate the group name ( $filename =~ /[\\\/](\w+).vmf/ ); my @pieces = ( $1 =~ /([^_]*)/g ); foreach( @pieces ) { $groupname = join( '', $groupname, ucfirst( $_ ) ); } } open( OUTFILE, ">", $outputname ) || die "Couldn't open output file: $outputname\n"; my @entityTables; my %entityNames; my @PrecacheNames; my %DoNotSpawnNames; my %ReplaceParmDefaults; # point_template spawn flag my $SF_PRESERVE_TEMPLATE_ENTITIES = 1; my $uniqueInstanceIdx = 1; # Process the vmf and convert entity spawn keys into squirrel table format my @lines = ReadVMF( $filename ); my @entities = ExtractEntityBlocks( \@lines ); ProcessEntityBlocks( \@entities ); # Build the spawn table my @SpawnNames; for my $key ( keys %entityNames ) { if ( !exists $DoNotSpawnNames{$key} ) { my $value = $entityNames{$key}; while ( $value > 0 ) { push( @SpawnNames, "EntityGroup.SpawnTables.$key$value,\n" ); --$value; } push( @SpawnNames, "EntityGroup.SpawnTables.$key,\n" ); } } $filename =~ /(\w+\.vmf)/; # Generate the output file print( OUTFILE "//-------------------------------------------------------\n" ); print( OUTFILE "// Autogenerated from '$1'\n" ); print( OUTFILE "//-------------------------------------------------------\n" ); print( OUTFILE "$groupname <-\n{\n" ); print( OUTFILE "\t//-------------------------------------------------------\n" ); print( OUTFILE "\t// Required Interface functions\n" ); print( OUTFILE "\t//-------------------------------------------------------\n" ); print( OUTFILE "\tfunction GetPrecacheList()\n\t{\n" ); print( OUTFILE "\t\tlocal precacheModels =\n\t\t[\n" ); foreach( @PrecacheNames ) { print( OUTFILE "\t\t\tEntityGroup.SpawnTables.$_,\n" ); } print( OUTFILE "\t\t]\n\t\treturn precacheModels\n\t}\n\n" ); print( OUTFILE "\t//-------------------------------------------------------\n" ); print( OUTFILE "\tfunction GetSpawnList()\n\t{\n" ); print( OUTFILE "\t\tlocal spawnEnts =\n\t\t[\n" ); foreach( @SpawnNames ) { print( OUTFILE "\t\t\t$_" ); } print( OUTFILE "\t\t]\n\t\treturn spawnEnts\n\t}\n\n" ); print( OUTFILE "\t//-------------------------------------------------------\n" ); print( OUTFILE "\tfunction GetEntityGroup()\n\t{\n\t\treturn EntityGroup\n\t}\n\n" ); print( OUTFILE "\t//-------------------------------------------------------\n" ); print( OUTFILE "\t// Table of entities that make up this group\n" ); print( OUTFILE "\t//-------------------------------------------------------\n" ); print( OUTFILE "\tEntityGroup =\n\t{\n" ); print( OUTFILE "\t\tSpawnTables =\n\t\t{\n" ); foreach ( @entityTables ) { print( OUTFILE @$_ ); } print( OUTFILE "\t\t} // SpawnTables\n" ); if ( %ReplaceParmDefaults > 0 ) { print( OUTFILE "\n\t\tReplaceParmDefaults =\n\t\t{\n" ); foreach ( keys %ReplaceParmDefaults ) { print( OUTFILE "\t\t\t\"$_\" : \"$ReplaceParmDefaults{ $_ }\"\n" ); } print( OUTFILE "\t\t}\n" ); } print( OUTFILE "\t} // EntityGroup\n" ); print( OUTFILE "} // $groupname\n\n" ); print( OUTFILE "RegisterEntityGroup( \"$groupname\", $groupname )\n" ); close( OUTFILE ); ####################################### package EntityHash; sub new { my($class) = shift; my($hash) = shift; bless { "ents" => $hash }, $class } sub GetKeys { my $self = shift; return @{ $self->{'ents'}{ 'keys' } }; } sub GetValue { my ($self, $key) = @_; return $self->{'ents'}{ $key }; } sub SetValue { my ($self, $key, $value) = @_; $self->{'ents'}{ $key } = $value; my @keys = @{ $self->{'ents'}{ 'keys' } }; foreach ( @keys ) { if ( $_ eq $key ) { return; } } push @{ $self->{'ents'}{ 'keys' } }, $key; } package main; ####################################### sub ReadVMF { my $filename = shift; open( INFILE, "<", $filename ) || die "Couldn't open file: $filename\n"; my @file = ; close ( INFILE ); return @file; } ####################################### sub ExtractEntityBlocks { my $input = shift; my @entities; while ( @$input ) { my $line = shift @$input; if ( $line =~ /^\t*(\w+)$/ ) { my %result = ExtractBlockToHash( \@$input ); if ( $1 eq "entity" ) { push @entities, \%result; } } } return @entities; } ####################################### sub ProcessEntityBlocks { my $entities = shift; my $replacementValues = shift; foreach ( @$entities ) { my @entityBlock = ProcessEntityBlock( $_, $replacementValues ); if ( @entityBlock > 0 ) { push( @entityTables, [ @entityBlock ] ); } } } ####################################### sub ExtractBlockToArray { my $input = shift; my $depth = 0; my @newblock; do { my $line = shift @$input; if ( $line =~ /{/ ) { ++$depth; } elsif ( $line =~ /}/ ) { --$depth; } push( @newblock, $line ); } while ( $depth > 0 ); return @newblock; } ####################################### sub ExtractBlockToHash { my $input = shift; my $depth = 0; my %newblock; my @keys; do { my $line = shift @$input; if ( $line =~ /{/ ) { ++$depth; } elsif ( $line =~ /}/ ) { --$depth; } elsif ( $line =~ /^\t*(\w+)$/ ) { # sub-block within the entity my $key = $1; # FIXME: Can't trust that nested blocks have unique names, so for now just extract them into arrays my @result = ExtractBlockToArray( \@$input ); $newblock{ $key } = \@result; push @keys, $key; } elsif ( $line =~ /^\t*\"(\w+)\" \"(.+)\"$/ ) { # Key/Value pair my $key = $1; my $value = $2; $newblock{ $key } = $value; push @keys, $key; } } while ( $depth > 0 ); $newblock{ "keys" } = [ @keys ]; return %newblock; } ####################################### sub CacheEntityName { my $entname = shift; $entname =~ s/[\@]//g; if ( exists $entityNames{$entname} ) { my $count = $entityNames{$entname}; $entityNames{$entname} = ++$count; $entname = "$entname$count"; } else { $entityNames{$entname} = 0; } return $entname } ####################################### sub PrecacheEntity { my $entname = shift; push( @PrecacheNames, $entname ); } ####################################### sub ProcessEntityBlock { my $EntData = new EntityHash( shift ); my $replacementValues = shift; my $entname = "unnamed"; my $classname; my $value; my @spawnData; # spawnkeys that need to be converted to Vector() my @vectorKeys = ( "origin", "angles", "pushdir", "springaxis", "axis" ); # Generate the entity spawn table using squirrel syntax if ( $classname = $EntData->GetValue( "classname" ) ) { my $classnameChanged = 0; # Ignore some entities entirely if ( $classname eq "func_detail" || $classname eq "info_overlay" || $classname eq "light_spot" ) { print( "Ignoring unsupported entity: $classname\n" ); return; } elsif ( $classname eq "func_instance_parms" ) { # Save off the replacement value defaults my $idx = 1; while ( $value = $EntData->GetValue( "parm$idx" ) ) { if ( $value =~ /^(\$\w+) (\w+) (\w+)/ ) { if ( !$ReplaceParmDefaults{ $1 } ) { $ReplaceParmDefaults{ $1 } = $3; } } ++$idx; } return; } # Flatten nested instances elsif ( $classname eq "func_instance" ) { ProcessNestedInstance( $EntData ); return; } # convert some entities to their script version elsif ( $classname =~ /^(trigger.*)/ && $classname ne "trigger_finale" ) { $classname =~ s/trigger/script_trigger/; $classnameChanged = 1; } elsif ( $classname eq "func_nav_blocker" ) { $classname = "script_nav_blocker"; $classnameChanged = 1; } elsif ( $classname eq "point_template" ) { $classname = "point_script_template"; $classnameChanged = 1; # Collect names of template-spawned entities so we can remove them from the spawn table my $spawnflags = $EntData->GetValue( "spawnflags" ); if ( !($spawnflags & $SF_PRESERVE_TEMPLATE_ENTITIES) ) { my @keys = @{ $EntData->GetValue( "keys" ) }; foreach my $key ( @keys ) { if ( $key =~ /Template\d{2}/ ) { my $name = $EntData->GetValue( $key ); $DoNotSpawnNames{ $name } = 0; } } } } if ( $classnameChanged ) { my $prev = $EntData->GetValue( "classname" ); print( "Converting $prev to $classname\n" ); $EntData->SetValue( "classname", $classname ); } } else { print( "WARNING: Couldn't find entity classname!\n" ); return; } # Cache off the entity name if ( $value = $EntData->GetValue( "targetname" ) ) { $entname = CacheEntityName( $value ); } else { $entname = CacheEntityName( $entname ); print( "Found $classname without a targetname (assigned group name: $entname)\n" ); } # Convert keys such as origin and angles to actual vector types instead of strings foreach( @vectorKeys ) { my $keyname = $_; if ( $value = $EntData->GetValue( $keyname ) ) { $value =~ s/^([\d\-e\.]+) ([\d\-e\.]+) ([\d\-e\.]+)$/Vector\( $1, $2, $3 \)/; $EntData->SetValue( $keyname, $value ); } } # See if this entity needs to be precached if ( $classname eq "point_spotlight" || $classname eq "env_explosion" ) { PrecacheEntity( $entname ); } elsif ( $value = $EntData->GetValue( "model" ) ) { if ( $value =~ /.*\.mdl|\.vmt/ ) { PrecacheEntity( $entname ); } } # Convert trigger brushes to extents if ( ($value = $EntData->GetValue( "solid" )) && (ref( $value ) eq "ARRAY") ) { if ( $classname =~ /^script_.*/ ) { my ($x, $y, $z, $cx, $cy, $cz ) = ProcessBrushData( \@$value ); $EntData->SetValue( "extent", "Vector( $x, $y, $z )" ); # Some brush entities such as func_nav_blocker don't have an origin, but the script version needs it. if ( !$EntData->GetValue( "origin" ) ) { $EntData->SetValue( "origin", "Vector( $cx, $cy, $cz )" ); } } else { print( "Stripping brush data from entity $classname ($entname)\n" ); } } # Do instance key replacments my @keys = $EntData->GetKeys(); foreach my $key ( @keys ) { my $value = $EntData->GetValue( $key ); if ( defined $replacementValues->{ $value } ) { $EntData->SetValue( $key, $replacementValues->{ $value } ); } } # Output the final spawn table my @keys = $EntData->GetKeys(); foreach my $key ( @keys ) { # Ignore some keys if ( $key eq "id" ) { next; } $value = $EntData->GetValue( $key ); if ( ref( $value ) ne "ARRAY" ) { # FIXME: tabs! if ( $value =~ /^Vector\(/ ) { push( @spawnData, "\t\t\t\t\t$key = $value\n" ); } else { push( @spawnData, "\t\t\t\t\t$key = \"$value\"\n" ); } } } # Handle the connections block if ( ($value = $EntData->GetValue( "connections" )) && (ref( $value ) eq "ARRAY") ) { push( @spawnData, ProcessConnectionsBlock( \@$value ) ); } my @entityTable; push ( @entityTable, "\t\t\t" ); push ( @entityTable, $entname ); push ( @entityTable, " = \n\t\t\t{\n\t\t\t\tSpawnInfo =\n" ); push ( @entityTable, "\t\t\t\t{\n" ); push ( @entityTable, @spawnData ); push ( @entityTable, "\t\t\t\t}\n" ); push ( @entityTable, "\t\t\t}\n" ); return @entityTable; } ###################################################### sub ProcessNestedInstance { #FIXME: Need to fixup connections as well! my $EntData = shift; my @instanceOrigin = ( 0, 0, 0 ); my @instanceAngles = ( 0, 0, 0 ); my $instanceFixupName = "InstanceAuto"; my $instanceFixupNum = $uniqueInstanceIdx; my $instanceFixupStyle = 2; my $value; # Collect the func_instance data and load the vmf if ( $value = $EntData->GetValue( "origin" ) ) { @instanceOrigin = ( $value =~ /([\d\-e\.]+)/g ); } if ( $value = $EntData->GetValue( "angles" ) ) { @instanceAngles = ( $value =~ /([\d\-e\.]+)/g ); } if ( $value = $EntData->GetValue( "fixup_style" ) ) { $instanceFixupStyle = $value; } if ($value = $EntData->GetValue( "targetname" ) ) { $instanceFixupName = $value; $instanceFixupNum = undef; } my $propagateFixup = $EntData->GetValue( "propagate_fixup" ); if ( defined $instanceFixupNum ) { ++$uniqueInstanceIdx; } # Get replacement values from this instance my %replacementValues; my @keys = $EntData->GetKeys(); foreach my $key ( @keys ) { if ( $key =~ /replace\d{2}/ ) { my $value = $EntData->GetValue( $key ); $value =~ /^(\$.+) (.+)/; $replacementValues{ $1 } = $2; } } # Build a matrix to represent the position/orientation of the func_instance my @instanceTransform = SetupMatrixOrgAngles( \@instanceOrigin, \@instanceAngles ); # Load the instance file my @instanceLines; if ( $value = $EntData->GetValue( "file" ) ) { @instanceLines = ReadVMF( "$path$value" ); } my @entities = ExtractEntityBlocks( \@instanceLines ); # Transform position/orientation for each entity in the instance my @instanceEntities; my @finalTransform; foreach my $entity ( @entities ) { # initialize the transform matrix @finalTransform = @instanceTransform; my $ChildData = new EntityHash( $entity ); my $value; if ( $value = $ChildData->GetValue( "targetname" ) ) { if ( $value !~ /^@/ ) { # Do instance name fixup my $fixedName = $value; if ( $instanceFixupStyle == 0 ) { $fixedName = "$instanceFixupName$instanceFixupNum\_$value"; } elsif ( $instanceFixupStyle == 1 ) { $fixedName = "$value\_$instanceFixupName$instanceFixupNum"; } $ChildData->SetValue( "targetname", "$fixedName" ); } } if ( $propagateFixup ) { if ( $value = $ChildData->GetValue( "classname" ) ) { if ( $value eq "func_instance" ) { # Pass down our settings to the nested instance $ChildData->SetValue( "fixup_style", $instanceFixupStyle ); $ChildData->SetValue( "targetname", "$instanceFixupName$instanceFixupNum" ); } } } if ( $value = $ChildData->GetValue( "origin" ) ) { my @entPos = ( $value =~ /([\d\-e\.]+)/g ); $entPos[3] = 1; my @newPos = ApplyTransform( \@instanceTransform, \@entPos ); $ChildData->SetValue( "origin", "$newPos[0] $newPos[1] $newPos[2]" ); } if ( $value = $ChildData->GetValue( "angles" ) ) { my @origin = ( 0, 0, 0 ); my @angles = ( $value =~ /([\d\-e\.]+)/g ); my @entTransform = SetupMatrixOrgAngles( \@origin, \@angles ); @finalTransform = ConcatTransforms( \@instanceTransform, \@entTransform ); @angles = MatrixAngles( \@finalTransform ); $ChildData->SetValue( "angles", "$angles[0] $angles[1] $angles[2]" ); } if ( $value = $ChildData->GetValue( "springaxis" ) ) { my @entPos = ( $value =~ /([\d\-e\.]+)/g ); $entPos[3] = 1; my @newPos = ApplyTransform( \@instanceTransform, \@entPos ); $ChildData->SetValue( "springaxis", "$newPos[0] $newPos[1] $newPos[2]" ); } if ( $value = $ChildData->GetValue( "axis" ) ) { my @entPos = ( $value =~ /([\d\-e\.]+)/g ); $entPos[3] = 1; my @newPos = ApplyTransform( \@instanceTransform, \@entPos ); $ChildData->SetValue( "axis", "$newPos[0] $newPos[1] $newPos[2]" ); } if ( $value = $ChildData->GetValue( "solid" ) ) { foreach my $line ( @$value ) { if ( $line =~ /^\t*\"(\w+)\" \"(.*)\"/ ) { my $key = $1; my $value = $2; if ( $key eq "plane" ) { # transform planes for brush entities (a plane is made up of three vectors) my @planes = ( $value =~ /([\d\-e\.]+)/g ); my @newplanes; for my $i ( 0 .. 2 ) { my $vtx = $i * 3; my @plane = @planes[$vtx .. $vtx+2]; my @tempplanes = ApplyTransform( \@finalTransform, \@plane ); push @newplanes, @tempplanes[0 .. 2]; } $line =~ s/\Q$value/($newplanes[0] $newplanes[1] $newplanes[2]) ($newplanes[3] $newplanes[4] $newplanes[5]) ($newplanes[6] $newplanes[7] $newplanes[8])/; } } } } } ProcessEntityBlocks( \@entities, \%replacementValues ); } ####################################### sub SetupMatrixOrgAngles { my $origin = shift; my $angles = shift; my @matrix; my @radians; for ( my $i = 0; $i < 3; ++$i ) { $radians[$i] = deg2rad( $angles->[$i] ); } my $sp = sin( $radians[0] ); my $cp = cos( $radians[0] ); my $sy = sin( $radians[1] ); my $cy = cos( $radians[1] ); my $sr = sin( $radians[2] ); my $cr = cos( $radians[2] ); $matrix[0][0] = $cp*$cy; $matrix[1][0] = $cp*$sy; $matrix[2][0] = -$sp; $matrix[0][1] = $sr*$sp*$cy+$cr*-$sy; $matrix[1][1] = $sr*$sp*$sy+$cr*$cy; $matrix[2][1] = $sr*$cp; $matrix[0][2] = ($cr*$sp*$cy+-$sr*-$sy); $matrix[1][2] = ($cr*$sp*$sy+-$sr*$cy); $matrix[2][2] = $cr*$cp; $matrix[0][3] = $origin->[0]; $matrix[1][3] = $origin->[1]; $matrix[2][3] = $origin->[2]; $matrix[3][0] = 0; $matrix[3][1] = 0; $matrix[3][2] = 0; $matrix[3][3] = 1; return @matrix; } ####################################### sub MatrixAngles { my $matrix = shift; my @forward = ( $matrix->[0][0], $matrix->[1][0], $matrix->[2][0] ); my @left = ( $matrix->[0][1], $matrix->[1][1], $matrix->[2][1] ); my @up = ( 0, 0, $matrix->[2][2] ); my $xyDist = sqrt( $forward[0] * $forward[0] + $forward[1] * $forward[1] ); my @angles; # enough here to get angles? if ( $xyDist > 0.001 ) { $angles[0] = rad2deg( atan2( -$forward[2], $xyDist ) ); $angles[1] = rad2deg( atan2( $forward[1], $forward[0] ) ); $angles[2] = rad2deg( atan2( $left[2], $up[2] ) ); } else # forward is mostly Z, gimbal lock- { $angles[0] = rad2deg( atan2( -$forward[2], $xyDist ) ); $angles[1] = rad2deg( atan2( -$left[0], $left[1] ) ); $angles[2] = 0; } return @angles; } ####################################### sub ConcatTransforms { my $mat1 = shift; my $mat2 = shift; my @outMat; for my $row ( 0 .. 3 ) { for my $col ( 0 .. 3 ) { my $sum = 0; for my $i ( 0 .. 3 ) { $sum += $mat1->[$row][$i] * $mat2->[$i][$col]; } $outMat[$row][$col] = $sum; } } return @outMat; } ####################################### sub ApplyTransform { my $matrix = shift; my $vector = shift; my @result = ( 0, 0, 0, 0 ); for my $row ( 0 .. 3 ) { my $sum = 0; for my $col ( 0 .. 3 ) { $sum += $matrix->[$row][$col] * $vector->[$col]; } $result[$row] = $sum; } return @result; } ####################################### sub PrintMatrix { my $input = shift; for my $row(0..3) { for my $col (0..3) { print( "$input->[$row][$col] " ); } print( "\n" ); } } ####################################### sub ProcessConnectionsBlock { my $input = shift; my @connections; my $output = "unset"; # re-format the connections block by turning each named output into its own sub-table push( @connections, "\t\t\t\t\tconnections =\n\t\t\t\t\t{\n" ); my %outputs; foreach my $line ( @$input ) { if ( $line =~ /\"(\w+)\" \"(.*)\"/ ) { push( @{ $outputs{ $1 } }, $2 ); } } foreach my $output ( keys %outputs ) { push( @connections, "\t\t\t\t\t\t$output =\n\t\t\t\t\t\t{\n" ); my $cmdnum = 1; foreach my $i ( 0 .. $#{ $outputs{$output} } ) { push( @connections, "\t\t\t\t\t\t\tcmd$cmdnum = \"$outputs{$output}[$i]\"\n" ); ++$cmdnum; } push( @connections, "\t\t\t\t\t\t}\n" ); } push( @connections, "\t\t\t\t\t}\n" ); return @connections; } ####################################### sub ProcessBrushData { my $input = shift; my @mins = ( 0, 0, 0 ); my @maxs = ( 0, 0, 0 ); my @norms = ( 0, 0, 0 ); my @origin = ( 0, 0, 0 ); my $init = 1; foreach my $line ( @$input ) { if ( $line =~ /^\t*\"plane\" \"(.*)\"/ ) { my $planes = $1; my @values = ( $planes =~ /([\d\-e\.]+)/g ); if ( $init ) { for ( my $i = 0; $i < 3; ++$i ) { $mins[$i] = $values[$i]; $maxs[$i] = $values[$i]; } $init = 0; } for ( my $vtx = 1; $vtx < 3; ++$vtx ) { for ( my $coord = 0; $coord < 3; ++$coord ) { my $test = @values[ $vtx * 3 + $coord ]; if ( $test < $mins[$coord] ) { $mins[$coord] = $test; } if ( $test > $maxs[$coord] ) { $maxs[$coord] = $test; } } } } } for ( my $i = 0; $i < 3; ++$i ) { $norms[$i] = ($maxs[$i] - $mins[$i]) / 2; $origin[$i] = $mins[$i] + $norms[$i]; } return @norms, @origin; }