#!/usr/bin/perl -w
# -------------
# DM2 Processor
# -------------
# With this Perl script you can manipulate frames and skins in ranges of
# blocks in DM2 files. It works on DM2 text files and (if LMPC is available),
# also on DM2 binary files.
# Get dm2proc (without HTML decorations) from
# http://demospecs.half-empty.de/misc/dm2proc.pl
# or from the CVS repository
# http://cvs.sourceforge.net/viewcvs.py/lmpc/lmpc/bin/procdm2.pl.
# $Id$
# Uwe Girlich (uwe@half-empty.de)
# modules init
use strict;
use Getopt::Long qw(:config no_ignore_case);
use IO::File;
use Pod::Usage;
use Pod::Text;
use IPC::Open3;
use File::Spec;
# prototypes
sub command_parse($);
sub logging;
sub warning;
sub syntaxerror($$$);
sub range_parse($);
# release information
sub release() { (my $release = q$Revision$) =~ s/^[^:]+:\s*(.*?)\s*$/$1/; $release; }
sub date() { (my $date = q$Date$) =~ s/^[^:]+:\s*(.*?)\s*$/$1/; $date; }
sub comment() { "alpha quality"; }
# default option values
my $opt_input = "input.dm2"; # default input file name
my $opt_output = "output.dm2"; # default output file name
my $opt_command = "command.proc"; # default processing command file
my $opt_lmpc = "lmpc"; # default lmpc command
my $opt_version = 0; # version off
my $opt_help = 0; # help off
my $opt_man = 0; # manual off
my $opt_logging = 0; # logging verbose level
my $opt_warning = 0; # warning verbose level
# parse command line
GetOptions(
'input|i=s' => \$opt_input,
'output|o=s' => \$opt_output,
'command|c=s' => \$opt_command,
'lmpc=s' => \$opt_lmpc,
'version|V' => \$opt_version,
'help|?' => \$opt_help,
'man|m' => \$opt_man,
'logging|l=i' => \$opt_logging,
'warning|w=i' => \$opt_warning,
) or pod2usage(-msg=>"Syntax error", -verbose=>0);
if ($opt_help) {
pod2usage(-verbose=>0);
warning 10, "podusage() for short help should not return.\n";
exit(1);
}
if ($opt_man) {
pod2usage(-verbose=>2);
warning 10, "podusage() for long help should not return.\n";
exit(1);
}
if ($opt_version) {
printf "DM2 Processor %s, %s (%s)\n", release(), date(), comment();
exit(1);
}
my $command = command_parse($opt_command);
if (not ref $command) {
warning 0, "Problem parsing command file $opt_command.\n";
exit(1);
}
logging 10, "parsing command file $opt_command completed.\n";
my $prepost = 0;
my $lmpc_ok = 0;
if (-f $opt_lmpc && -x $opt_lmpc) {
logging 30, "$opt_lmpc is an executable.\n";
# but maybe it is without directory and not in the PATH
my ($volume,$directories,$file) = File::Spec->splitpath($opt_lmpc);
if ($volume eq "" && $directories eq "") {
# single filename, must be in PATH or not callable
my @path = File::Spec->path();
foreach (@path) {
my $abs_path = File::Spec->catfile($_,$opt_lmpc);
if (-f $abs_path && -x $abs_path) {
logging 20, "$opt_lmpc was found in PATH env.\n";
$lmpc_ok = 1;
}
}
if ($lmpc_ok == 0) {
logging 20, "$opt_lmpc was not found in PATH env.\n";
}
}
else {
if (File::Spec->file_name_is_absolute($opt_lmpc)) {
logging 20, "$opt_lmpc is absolute path.\n";
$lmpc_ok = 1;
}
else {
logging 20, "$opt_lmpc is relative path.\n";
$lmpc_ok = 1;
}
}
}
else {
my ($volume,$directories,$file) = File::Spec->splitpath($opt_lmpc);
if ($volume eq "" && $directories eq "") {
my @path = File::Spec->path();
foreach (@path) {
my $abs_path = File::Spec->catfile($_,$opt_lmpc);
if (-f $abs_path && -x $abs_path) {
logging 20, "$opt_lmpc was found in PATH env.\n";
$lmpc_ok = 1;
}
}
}
else {
logging 20, "$opt_lmpc does not exist.\n";
$lmpc_ok = 0;
}
}
if ($lmpc_ok == 1) {
logging 20, "The command $opt_lmpc is available.\n";
my $info = "$opt_lmpc --info $opt_input";
logging 30, "checking with '$info', if input is text or binary\n";
my $info_fh;
my $pid = open3("<&STDIN", $info_fh, $info_fh, $info);
if ($pid==0) {
warning 0, "Could not execute ´$info´: $!.\n";
exit(1);
}
logging 35, "started child with pid $pid.\n";
if (!defined $info_fh) {
warning 0, "Could not read output from '$info': $!.\n";
waitpid $pid, 0;
exit(1);
}
logging 40, "reading output from '$info'\n";
while (<$info_fh>) {
if (/$opt_input.*DM2 txt/) {
$prepost = 0;
logging 35, "$opt_input is really text\n";
last;
}
if (/$opt_input.*DM2 bin/) {
logging 35, "$opt_input is really binary\n";
$prepost = 1;
last;
}
}
$info_fh->close();
logging 40, "output ended\n";
waitpid $pid, 0;
}
else {
warning 20, "The command $opt_lmpc is not available.\n";
$prepost = 0;
}
my $text_in;
my $text_out;
if ($prepost) {
logging 35, "$opt_input is assumed to contained binary\n";
$text_in = "text_in_$$.txt";
$text_out = "text_out_$$.txt";
}
else {
logging 35, "$opt_input is assumed to contain text\n";
$text_in = $opt_input;
$text_out = $opt_output;
}
my $error = 0;
if ($prepost) {
my $preproc = "$opt_lmpc --to-txt $opt_input $text_in";
logging 20, "Calling '$preproc' to generate text file.\n";
system $preproc;
if (!-f $text_in) {
warning 0, "LMPC ($opt_lmpc) did not create $text_in.\n";
$error = 1;
goto out;
}
}
logging 0, "$text_in (DM2 txt) -> $text_out (DM2 txt)\n";
logging 30, "Reading $text_in.\n";
my $in_fh = new IO::File "<$text_in";
if (!defined $in_fh) {
warning 0, "Could not open $text_in for reading: $!.\n";
if (!$prepost) {
warning 0, "Please use '-i inputfile'.\n";
}
$error = 1;
goto out;
}
logging 30, "Writing $text_out.\n";
my $out_fh = new IO::File ">$text_out";
if (!defined $out_fh) {
warning 0, "Could not open $text_out for writing: $!.\n";
$error = 1;
goto out;
}
my $state = 0;
my $block = 0;
my $entity = 1;
my $frame = 0;
my $skin = 0;
my $new_frame = 0;
my $new_skin = 0;
my $subst_frame = 0;
my $subst_skin = 0;
my %entity = ();
while (<$in_fh>) {
my $line = $_;
# process the input line
if ($line =~ /^ seq1 (\d+)/) {
$block = $1;
logging 30, "block number $block: ";
if (!exists $command->{$block}) {
logging 30, "ignore\n";
$state = 0;
}
else {
logging 30, "work\n";
$state = 1;
# Memorize all the entities where we have something
# to do in this frame.
%entity=();
for (keys %{$command->{$block}}) {
$entity{$_} = 1;
}
}
}
if ($state == 1) {
if ($line =~ /packetentities {/) {
logging 40, "block number $block packetentities start\n";
$state = 2;
}
}
if ($state == 2) {
if ($line =~ / packetentity {/) {
logging 40, "block number $block packetentity start\n";
$state = 3;
}
if ($line =~ /^ }/) {
logging 40, "block number $block packetentites ended\n";
for $entity (keys %entity) {
logging 40, "block $block, entity $entity missing\n";
my $line_new = " packetentity {\n entity $entity;\n";
if (exists $command->{$block}->{$entity}->{"f"}) {
$frame = $command->{$block}->{$entity}->{"f"};
logging 40, "adding frame $frame\n";
$line_new .= " frame $frame;\n";
}
if (exists $command->{$block}->{$entity}->{"s"}) {
$skin = $command->{$block}->{$entity}->{"s"};
logging 40, "adding skin $frame\n";
$line_new .= " skin $skin;\n";
}
$line = $line_new . " }\n" . $line;
}
%entity=();
$state = 0;
}
}
if ($state == 3) {
if ($line =~ / entity (\d+);/) {
$entity = $1;
logging 30, "entity number $entity: ";
if (!exists $command->{$block}->{$entity}) {
logging 30, "ignore\n";
$state = 2;
}
else {
logging 30, "work\n";
$state = 4;
if (exists $command->{$block}->{$entity}->{"f"}) {
$frame = $command->{$block}->{$entity}->{"f"};
$new_frame = 1;
$subst_frame = 0;
logging 31, "frame -> $frame\n";
}
if (exists $command->{$block}->{$entity}->{"s"}) {
$skin = $command->{$block}->{$entity}->{"s"};
$new_skin = 1;
$subst_skin = 0;
logging 31, "skin -> $skin\n";
}
}
}
}
if ($state == 4) {
if ($new_frame &&
$line =~ s/( frame )\d+(;)/$1$frame$2/) {
logging 32, "frame subst OK\n";
$subst_frame = 1;
}
if ($new_skin &&
$line =~ s/( skin )\d+(;)/$1$skin$2/) {
logging 32, "skin subst OK\n";
$subst_skin = 1;
}
if ($line =~ / }/) {
if ($new_frame && !$subst_frame) {
$line = " frame $frame;\n" . $line;
logging 32, "frame append OK\n";
}
if ($new_skin && !$subst_skin) {
$line = " skin $skin;\n" . $line;
logging 32, "skin append OK\n";
}
logging 40, "packetentity ended\n";
$state = 2;
}
delete $entity{$entity}; # This is done.
}
print $out_fh $line;
}
$in_fh->close();
$out_fh->close();
if ($prepost) {
my $postproc = "$opt_lmpc --to-bin $text_out $opt_output";
logging 20, "Calling '$postproc' to generate binary file.\n";
system $postproc;
}
out:
if ($prepost) {
logging 20, "Remove temporary text files $text_in and $text_out.\n";
unlink $text_in, $text_out;
}
# End of program.
exit $error;
sub command_parse($)
{
my $command_filename = shift;
my %command = ();
logging 0, "Reading command file %s.\n", $command_filename;
my $command_fh=new IO::File("<$command_filename");
if (!defined $command_fh) {
warning 0, "Could not open %s for reading: $!.
Please use '-c commandfile'.\n", $command_filename;
return 0;
}
my $e=0;
while (<$command_fh>) {
my $line = $_;
$line =~ s/#.*//; # remove comment
$line =~ s/^\s*//; # remove leading whitespaces
$line =~ s/\s*$//; # remove trailing whitespaces
$line =~ s/\s*-/-/g; # remove whitespace before -
$line =~ s/-\s*/-/g; # remove whitespace after -
next if $line =~ /^$/; # ignore empty line
my $letter;
my $args;
if ($line =~ /^([a-zA-Z])\s*(.*)$/) {
$letter = lc $1;
$args = $2;
}
else {
syntaxerror $command_filename, $., "no command";
return 0;
}
if ($letter eq "e") {
if ($args =~ /^(\d+)$/) {
$e = $1;
}
else {
syntaxerror $command_filename, $., "$args is not a number";
return 0;
}
logging 40, "entity $e\n";
}
elsif ($letter eq "f" or $letter eq "s") {
my $blocks;
my $frames;
if ($args =~ /^([^\s]+)\s+([^\s]+)$/) {
$blocks = $1;
$frames = $2;
}
else {
syntaxerror $command_filename, $., "command $letter: $args is not blockrange argsrange";
return 0;
}
my ($bs,$be,$bi) = range_parse($blocks);
my ($fs,$fe,$fi) = range_parse($frames);
logging 50, "blocks $bs ... $be (inc $bi): $letter $fs ... $fe (inc $fi)\n";
my $f = $fs;
for (my $b=$bs;$bi>0?$b<=$be:$b>=$be;$b+=$bi) {
$command{$b}{$e}{$letter}=$f;
logging 60, "b $b e $e $letter $f\n";
$f += $fi;
if ($fi>0?$f>$fe:$f<$fe) {
$f = $fs;
}
}
}
else {
syntaxerror $command_filename, $., "unknown command";
return 0;
}
}
$command_fh->close();
return \%command;
}
# trace
sub logging
{
my $level = shift;
if ($level<=$opt_logging) {
printf STDERR @_;
}
}
sub warning
{
my $level = shift;
if ($level<=$opt_warning) {
printf STDERR @_;
}
}
sub syntaxerror($$$)
{
my ($command, $line, $message) = @_;
warning 0, "Command file $command: syntax error in line $line.\n$message.\n";
}
sub range_parse($)
{
my $text = shift;
if ($text =~ /^([^-]+)-([^-]+)$/) {
return $1, $2, $1<$2? 1 : -1;
}
else {
return $text, $text, 1;
}
}
__END__
=head1 NAME
procdm2 - DM2 Processor
=head1 SYNOPSIS
procdm2.pl [options]
Options:
-i|--input file input file name (default: input.dm2).
-o|--output file output file name (default: output.dm2).
-c|--command file command file name (default: command.proc).
--lmpc file LMPC executable (default: lmpc)
-V|--version print version.
-h|-?|--help brief help message.
-m|--man full documentation.
-l|--logging level logging verbose level (default: 0, off).
-w|--warning level warning verbose level (default: 0, off).
=head1 OPTIONS
=over 8
=item B<--input file>
Defines the input file name. Default: input.dm2. The input file can be
a DM2 text file or a DM2 binary file. In case of a binary file, it will
be converted into a text file by calling LMPC --to-txt.
=item B<--output file>
Defines the output file name. Default: output.dm2. The output file has the
same type (binary or text) as the input file.
=item B<--command file>
Defines the processing command file name. Default: command.proc.
=item B<--lmpc file>
Defines the name of the LMPC command. Default: lmpc.
=item B<--version>
Prints the program version and exits.
=item B<--help>
Print a brief help message and exits.
=item B<--man>
Prints the manual page and exits.
=item B<--logging level>
Set the verbose level for logging trace output. Default: 0, off.
=item B<--warning level>
Set the verbose level for warning trace output. Default: 0, off.
=back
=head1 DESCRIPTION
This program will read the given input file, perform the actions defined in
the command file and write the output file.
=head1 PROCESSING COMMANDS
The processing command text file consists of lines with commands.
# starts a comment. Empty lines will be ignored.
=over 8
=item B<e entity>
This defines the number of the entity, for which the following commands
are valid. Defaults to 1.
=item B<f blockrange framerange>
This commands defines, that in the blocks in blockrange the frames from
framerange must be used.
=item B<s blockrange skinrange>
This commands defines, that in the blocks in blockrange the skins from
skinrange must be used.
=back
=head1 RANGES
A range consists of a starting number and optional a dash and an ending
number. Whitespaces and backward ordering (end smaller than begin) are
allowed.
=head1 SEE ALSO
L<http://demospecs.half-empty.de/demospecs/lmpc-stable>
L<http://demospecs.half-empty.de/misc/procdm2.html>
L<http://www.sourceforge.net/projects/lmpc>
=head1 AUTHOR
Uwe Girlich (uwe@half-empty.de)
=cut