#!/usr/bin/env perl

#---------------------------------------------------------------------
#                          file information
#---------------------------------------------------------------------

# Filename: mus2pwad
# Purpose:  Creates a Doom PWAD file based on a Doom MUS file
# License:  Creative Commons Attribution-NonCommercial-ShareAlike 2.5
# Revision: 211005

# Note: The license indicated above applies to this script only. "deu-
# tex", PRBoom, and FreeDoom are separate packages,  and are distribu-
# ted under their own licenses.

#---------------------------------------------------------------------
#                         license information
#---------------------------------------------------------------------

# This section may not be modified except as approved by the author or
# licensor, or to make non-content changes such as adjustments to par-
# agraph formatting or white space.

# This  version of this software is  distributed  under  the following
# license:
#
#       Creative Commons Attribution-NonCommercial-ShareAlike 4.0

# You may use, modify, and redistribute this software without fees  or
# royalties, but only under the terms and conditions set forth  by the
# license.  In particular, copies and derived works cannot be used for
# commercial purposes.  Additionally, the license propagates to copies
# and derived works. Furthermore, you must provide attribution "in the
# manner specified by the author or licensor".

# This is not a  complete explanation of the  terms and conditions in-
# volved.  For more information, see the Creative Commons Attribution-
# Noncommercial 4.0 license.

#---------------------------------------------------------------------
#                             explanation
#---------------------------------------------------------------------

# 1. "mus2pwad" is a  Doom 1/Doom 2 utility.  This program takes three
# arguments: (order doesn't matter):
#
#     --music=file1.mus --level=file2.wad --output=file3.wad

# All  three arguments are  required.  "--music" specifies  a Doom MUS
# (i.e., music)  input file.  "--level" specifies a  Doom PWAD  (i.e.,
# level) file.  "--output" specifies a  Doom  PWAD output file.  Note:
# Either absolute or relative paths may be used.

# "mus2pwad" tries to identify the specific Doom level associated with
# the PWAD input file.  (If the file contains more than one level,  an
# arbitrary selection is made.)

# If a level is identified,  this program creates  the  specified PWAD
# output file.  The  output  file can be used  to replace  the default
# music for the level in question with the specified music.

# Requirements:
#
#     a. This program requires a specific version of "deutex": release
#        5.2.2.
#
#     b. Additionally,  this program needs a special data file; speci-
#        fically, the "doom2.wad" bootstrap file provided by FreeDoom.
#        Note: The "doom2.wad" file in question is a minimal stub (not
#        the normal version of "doom2.wad").

#---------------------------------------------------------------------

# 2. Setup procedure:
#
#     a. Install "deutex" 5.2.2 in an appropriate "bin" directory. The
#        directory used must be accessible through PATH.
#
#     b. Put the "bootstrap" WAD file discussed previously in any con-
#        venient data directory.
#
#     c. Copy this script to an appropriate "bin" directory. Note: The
#        directory used must be accessible through PATH.
#
#     d. Use "chmod 755" to make the  new copy of this script executa-
#        ble.
#
#     e. Edit the new copy and modify the "program parameters" section
#        appropriately.

#---------------------------------------------------------------------
#                            module setup
#---------------------------------------------------------------------

require 5.10.1  ;
use strict      ;
use Carp        ;
use warnings    ;
use Cwd         ;
                                # Trap warnings
$SIG {__WARN__} = sub { die @_; };

#---------------------------------------------------------------------
#                           basic constants
#---------------------------------------------------------------------

use constant ZERO  => 0;        # Zero
use constant ONE   => 1;        # One

use constant FALSE => 0;        # Boolean FALSE
use constant TRUE  => 1;        # Boolean TRUE

#---------------------------------------------------------------------
#                         program parameters
#---------------------------------------------------------------------

#  $DIR_BOOTSTRAP should specify an  absolute path for  the  directory
# that contains the "bootstrap" WAD file discussed previously.

my $DIR_BOOTSTRAP = '__META_PREFIX__/data/bootstrap';

#---------------------------------------------------------------------

#  $DIR_TMP should  specify an  absolute path for a  unique temporary-
# file directory.  Ideally,  the specified directory should not exist.
# Warning:  If the directory does exist,  this program will attempt to
# delete it.

# For reasons  explained above, you may specify  a unique subdirectory
# of "/tmp" or "/var/tmp" here, but you should  not specify  "/tmp" or
# "/var/tmp" themselves.

# This program  creates the specified directory at runtime.  Note: The
# associated parent directory  (where the temporary directory  will be
# placed) should have 0777 permissions.

my $DIR_TMP = "/var/tmp/mus2pwad-$>-$$-delete";

#---------------------------------------------------------------------

#  $REVISION should specify an appropriate program-revision string.

my $REVISION = '211006';

#---------------------------------------------------------------------
#                          Doom 2 music map
#---------------------------------------------------------------------

#  %Doom2_MusicMap  maps Doom 2 level names (MAP01, etc.) to "deutex"-
# compatible "D_..." music-entry names.

my %Doom2_MusicMap =
(
    MAP01 => 'D_RUNNIN'     ,
    MAP02 => 'D_STALKS'     ,
    MAP03 => 'D_COUNTD'     ,
    MAP04 => 'D_BETWEE'     ,
    MAP05 => 'D_DOOM'       ,
    MAP06 => 'D_THE_DA'     ,
    MAP07 => 'D_SHAWN'      ,
    MAP08 => 'D_DDTBLU'     ,
    MAP09 => 'D_IN_CIT'     ,
    MAP10 => 'D_DEAD'       ,
    MAP11 => 'D_STLKS2'     ,
    MAP12 => 'D_THEDA2'     ,
    MAP13 => 'D_DOOM2'      ,
    MAP14 => 'D_DDTBL2'     ,
    MAP15 => 'D_RUNNI2'     ,
    MAP16 => 'D_DEAD2'      ,
    MAP17 => 'D_STLKS3'     ,
    MAP18 => 'D_ROMERO'     ,
    MAP19 => 'D_SHAWN2'     ,
    MAP20 => 'D_MESSAG'     ,
    MAP21 => 'D_COUNT2'     ,
    MAP22 => 'D_DDTBL3'     ,
    MAP23 => 'D_AMPIE'      ,
    MAP24 => 'D_THEDA3'     ,
    MAP25 => 'D_ADRIAN'     ,
    MAP26 => 'D_MESSG2'     ,
    MAP27 => 'D_ROMER2'     ,
    MAP28 => 'D_TENSE'      ,
    MAP29 => 'D_SHAWN3'     ,
    MAP30 => 'D_OPENIN'     ,
    MAP31 => 'D_EVIL'       ,
    MAP32 => 'D_ULTIMA'
);

#---------------------------------------------------------------------
#                          support routines
#---------------------------------------------------------------------

# "Usage" outputs a usage-error message and exits.

sub Usage
{
    print << "END";
mus2pwad $REVISION - Converts a Doom 1 or 2 MUS file to PWAD format

Usage: mus2pwad --music=file1.mus --level=file2.wad --output=file3.wad

"mus2pwad"  tries to identify the  specific Doom level associated with
"file2.wad". (If the file contains more than one level,  an  arbitrary
selection is made.)

If a level is identified,  this program creates  "file3.wad".  The WAD
produced can be used to replace the default music for "file2.wad" with
the specified music.

END
    exit ONE;
}

#---------------------------------------------------------------------

# Future change: Document this routine.

sub IdentifyWAD_Level
{
    my ($ifname) = @_;
    my $data;                   # Data buffer

    if (!defined ($ifname) || (!-f $ifname) ||
        !open (IFD, "<$ifname"))
    {
        return "unknown";
    }

    undef $/;
    binmode IFD;
    $data = <IFD>;
    $data = "" if !defined $data;
    close IFD;

    if ($data !~ m@\000\000(E\dM\d|MAP\d\d)\000\000@)
    {
        return "unknown";
    }

    $1;
}

#---------------------------------------------------------------------
#                            main routine
#---------------------------------------------------------------------

sub Main
{
    my $CWD = getcwd();         # Current working directory
    my $cmd;                    # Shell-level command string
    my $level;                  # Doom 1 [or 2] level identifier
    my $str;                    # Scratch

    my $FileMUS;                # Pathname for MUS  input  file
    my $FilePWAD_Input;         # Pathname for PWAD input  file
    my $FilePWAD_Output;        # Pathname for PWAD output file

#---------------------------------------------------------------------
# Initial setup.

    $CWD           =~ s@([^/])\z@$1/@;
    $DIR_BOOTSTRAP =~ s@([^/])\z@$1/@;

#---------------------------------------------------------------------
# Process the command line.

    for my $arg (@ARGV)
    {
        $FileMUS         = $1 if $arg =~ s@^--music=(.+\.mus)\z@@i;
        $FilePWAD_Input  = $1 if $arg =~ s@^--level=(.+\.wad)\z@@i;
        $FilePWAD_Output = $1 if $arg =~ s@^--output=(.+\.wad)\z@@i;
        &Usage() if length $arg;
    }

    &Usage() unless defined ($FileMUS) && defined ($FilePWAD_Input) &&
                    defined ($FilePWAD_Output);

#---------------------------------------------------------------------
# Check/adjust the input-file pathnames.

    $CWD =~ s@([^/])\z@$1/@;

    for my $ref_path (\$FileMUS, \$FilePWAD_Input)
    {
        my $ifname = $$ref_path;

        if ((!-f $ifname) || (-z $ifname) || (!-r $ifname))
        {
            print STDERR << "END";
Error: The following input file is missing, empty, or inaccessible:
$ifname
END
            exit ONE;
        }

        if ($ifname !~ m@^/@)
        {
            $$ref_path =  $CWD . $ifname;
            while ($$ref_path =~ s@/[^/]+/\.\./@/@g) {}
        }
    }

#---------------------------------------------------------------------
# Check/adjust the output-file pathname.

    for my $ref_path (\$FilePWAD_Output)
    {
        my $ofname = $$ref_path;

        if ((-e $ofname) || (-l $ofname))
        {
            print STDERR << "END";
Error:  The following output file already  exists  [or it's a symbolic
link]. If you want to replace the file,  delete it before running this
program:

$ofname
END
            exit ONE;
        }

        if ($ofname !~ m@^/@)
        {
            $$ref_path =  $CWD . $ofname;
            while ($$ref_path =~ s@/[^/]+/\.\./@/@g) {}
        }
    }

#---------------------------------------------------------------------
# Report final pathnames.

    print << "END";
MUS  input  file = $FileMUS
PWAD input  file = $FilePWAD_Input
PWAD output file = $FilePWAD_Output
END

#---------------------------------------------------------------------
# Try to determine Doom level.

    $level =  &IdentifyWAD_Level ($FilePWAD_Input);
    $level =  "" if !defined $level;
    $level =~ s@\s+\z@@;

    if ($level =~ m@^(E\dM\d|MAP\d\d)\z@)
    {
        print "$FilePWAD_Input: Level seems to be $level\n";
    }
    else
    {
        print STDERR << "END";
Error: $FilePWAD_Input: Unable to determine level
END
        exit ONE;
    }

#---------------------------------------------------------------------
# Create temporary directory.

    if ($DIR_TMP =~ m@^/\w+(/\w+|)/?\z@)
    {                           # Safety measure
        print STDERR << "END";
Internal error #0001: The following DIR_TMP setting is not allowed:
$DIR_TMP
END
        exit ONE;
    }

    system << "END";
rm -fr    $DIR_TMP
mkdir -p  $DIR_TMP
chmod 777 $DIR_TMP
END

    if ((!-d $DIR_TMP) ||
        (!-r $DIR_TMP) || (!-w $DIR_TMP) || (!-x $DIR_TMP))
    {
        print STDERR << "END";
Error: I wasn't able to reset the following temporary directory:
$DIR_TMP
END
        exit ONE;
    }

#---------------------------------------------------------------------
# Go to temporary directory.

    chdir ($DIR_TMP) ||
        die "Error: Can't enter temporary directory: $!:\n$DIR_TMP\n";

#---------------------------------------------------------------------
# Create items inside temporary directory.

    my $WADINFO = "wadinfo$$-$>.txt";

    if ($level =~ m@^E\dM\d\z@)
    {
        $str = "D_$level";
    }
    elsif ($level =~ m@^MAP\d\d\z@)
    {
        undef $str;
        $str = $Doom2_MusicMap {$level};

        if (!defined ($str))
        {
            die "Internal error #0002\n";
        }
    }
    else
    {
        die "Error: Unsupported Doom level: $level\n";
    }

    $str =~ s@\s*\n\s*@\n@gs;
    $str =~ s@\s+\z@@s;
    open (OFD, ">$WADINFO") || die "Error #0002\n";
    binmode OFD;
    print OFD << "END";
[musics]
$str
END
    close OFD;

    $str =  lc ($str);
    $str =~ s@\s+@ @gs;
    $str =~ s@\s+\z@@s;

    system << "END";
rm -fr    musics
mkdir -p  musics || exit 1
chmod 777 musics || exit 1
cd        musics || exit 1

for x in $str
do
    ln -nsf $FileMUS \$x.mus
done
END

#---------------------------------------------------------------------
# Execute an appropriate "deutex440" command.

    $cmd = << "END";
deutex -rgb 0 255 255
-doom2 $DIR_BOOTSTRAP
-patch -musics -build $WADINFO $FilePWAD_Output
END
    $cmd =~ s@\s+@ @gs;
    $cmd =~ s@\s+\z@@s;
    system $cmd;

#---------------------------------------------------------------------
# Wrap it up.

    chdir '/';                  # Leave  the temporary directory
    system "rm -fr $DIR_TMP";   # Remove the temporary directory

    if (-f $FilePWAD_Output)    # Created the output file?
    {                           # Yes
        print "Created $FilePWAD_Output\n";
    }
    else
    {                           # No  - Error
        die "Error: Operation failed\n";
    }

    undef;
}

#---------------------------------------------------------------------
#                            main program
#---------------------------------------------------------------------

&Main();                        # Call the main routine
exit ZERO;                      # Normal exit
