Initial commit 0.000_001
authorMarius Gavrilescu <marius@ieval.ro>
Sat, 9 Aug 2014 21:16:31 +0000 (00:16 +0300)
committerMarius Gavrilescu <marius@ieval.ro>
Sat, 9 Aug 2014 21:16:31 +0000 (00:16 +0300)
Changes [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
README [new file with mode: 0644]
app.psgi [new file with mode: 0644]
cfg/options.yml [new file with mode: 0644]
index.html [new file with mode: 0644]
lib/App/Web/VPKBuilder.pm [new file with mode: 0644]
static/index.css [new file with mode: 0644]
t/App-Web-VPKBuilder.t [new file with mode: 0644]

diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..c60f7eb
--- /dev/null
+++ b/Changes
@@ -0,0 +1,4 @@
+Revision history for Perl extension App::Web::VPKBuilder.
+
+0.000_001 2014-08-10T00:16+03:00
+ - Initial Release
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..ac6532e
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,10 @@
+app.psgi
+cfg/options.yml
+Changes
+index.html
+lib/App/Web/VPKBuilder.pm
+Makefile.PL
+MANIFEST
+README
+static/index.css
+t/App-Web-VPKBuilder.t
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..126b1b3
--- /dev/null
@@ -0,0 +1,39 @@
+use 5.014000;
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+       NAME              => 'App::Web::VPKBuilder',
+       VERSION_FROM      => 'lib/App/Web/VPKBuilder.pm',
+       ABSTRACT_FROM     => 'lib/App/Web/VPKBuilder.pm',
+       AUTHOR            => 'Marius Gavrilescu <marius@ieval.ro>',
+       MIN_PERL_VERSION  => '5.14.0',
+       LICENSE           => 'perl',
+       SIGN              => 1,
+       PREREQ_PM         => {
+               qw/File::Basename        0
+                  File::Find            0
+                  File::Path            0
+                  File::Spec::Functions 0
+                  File::Temp            0
+                  IO::Compress::Zip     0
+                  sigtrap               0
+
+                  Data::Diver       0
+                  File::Slurp       0
+                  HTML::Element     0
+                  HTML::TreeBuilder 0
+                  Hash::Merge       0
+                  List::MoreUtils   0
+                  Plack::Builder    0
+                  Plack::Component  0
+                  Plack::Request    0
+                  Sort::ByExample   0
+                  YAML              0/,
+       },
+       META_MERGE         => {
+               dynamic_config => 0,
+               resources      => {
+                       repository   => 'http://git.ieval.ro/?p=app-web-vpkbuilder.git',
+               }
+       }
+);
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..57106a9
--- /dev/null
+++ b/README
@@ -0,0 +1,39 @@
+App-Web-VPKBuilder version 0.000_001
+====================================
+
+App::Web::VPKBuilder is a simple web service for building Source
+engine game VPK packages. It presents a list of mods sorted into
+(sub)categories. The user can choose a mod from each category and will
+get a VPK containing all of the selected packages.
+
+INSTALLATION
+
+To install this module type the following:
+
+   perl Makefile.PL
+   make
+   make test
+   make install
+
+DEPENDENCIES
+
+This module requires these other modules and libraries:
+
+* Data::Diver
+* File::Slurp
+* HTML::Tree
+* Hash::Merge
+* List::MoreUtils
+* Plack
+* Sort::ByExample
+* YAML
+
+COPYRIGHT AND LICENCE
+
+Copyright (C) 2014 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.18.2 or,
+at your option, any later version of Perl 5 you may have available.
+
+
diff --git a/app.psgi b/app.psgi
new file mode 100644 (file)
index 0000000..573d903
--- /dev/null
+++ b/app.psgi
@@ -0,0 +1,14 @@
+#!/usr/bin/perl
+use v5.14;
+use warnings;
+
+use Plack::Builder;
+use App::Web::VPKBuilder;
+
+builder {
+       enable 'ContentLength';
+       enable Static => path => qr!^/static/!;
+       App::Web::VPKBuilder->new->to_app
+};
+
+__END__
diff --git a/cfg/options.yml b/cfg/options.yml
new file mode 100644 (file)
index 0000000..3cd4e64
--- /dev/null
@@ -0,0 +1,4 @@
+---
+readme: "Place the .vpk file in your custom directory (<steam root>/SteamApps/common/Team Fortress 2/tf/custom/)"
+sort_order: [Scout, Soldier, Pyro, Demoman, Heavy, Engineer, Medic, Sniper, Spy, Sounds, Model]
+dir: work
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..f049a85
--- /dev/null
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Title</title>
+<meta charset="utf-8">
+<link rel="stylesheet" href="/static/index.css">
+
+<h1>Select package components</h1>
+<form action="/makepkg">
+<div id="list"></div>
+<input type="submit" value="Make package">
+</form>
diff --git a/lib/App/Web/VPKBuilder.pm b/lib/App/Web/VPKBuilder.pm
new file mode 100644 (file)
index 0000000..cb3a96f
--- /dev/null
@@ -0,0 +1,248 @@
+package App::Web::VPKBuilder;
+
+use 5.014000;
+use strict;
+use warnings;
+use parent qw/Plack::Component/;
+our $VERSION = '0.000_001';
+
+use File::Basename qw/fileparse/;
+use File::Find qw/find/;
+use File::Path qw/remove_tree/;
+use File::Spec::Functions qw/catfile rel2abs/;
+use File::Temp qw/tempdir/;
+use IO::Compress::Zip qw/zip ZIP_CM_LZMA/;
+use sigtrap qw/die normal-signals/;
+
+use Data::Diver qw/DiveRef/;
+use File::Slurp qw/write_file/;
+use HTML::Element;
+use HTML::TreeBuilder;
+use Hash::Merge qw/merge/;
+use List::MoreUtils qw/uniq/;
+use Plack::Request;
+use Sort::ByExample qw/sbe/;
+use YAML qw/LoadFile/;
+
+sub new {
+       my $self = shift->SUPER::new(@_);
+       $self->{cfg} = {};
+       for (sort <cfg/*>) {
+               my $cfg = LoadFile $_;
+               $self->{cfg} = merge $self->{cfg}, $cfg
+       }
+       $self->{cfg}{vpk}           //= 'vpk';
+       $self->{cfg}{vpk_extension} //= 'vpk';
+       $self->{cfg}{sort} = sbe $self->{cfg}{sort_order}, { fallback => sub { shift cmp shift } };
+       $self
+}
+
+sub addpkg {
+       my ($pkg, $dir) = @_;
+       return unless $pkg =~ /^[a-zA-Z0-9_-]+$/aa;
+       my @dirs = ($dir);
+       find {
+               postprocess => sub { pop @dirs },
+               wanted => sub {
+                       my $dest = catfile @dirs, $_;
+                       mkdir $dest if -d;
+                       push @dirs, $_ if -d;
+                       link $_, $dest if -f;
+       }}, catfile 'pkg', $pkg;
+}
+
+sub makepkg {
+       my ($self, @pkgs) = @_;
+       mkdir $self->{cfg}{dir};
+       my $dir = rel2abs tempdir 'workXXXX', DIR => $self->{cfg}{dir};
+       my $dest = catfile $dir, 'pkg';
+       mkdir $dest;
+       push @pkgs, split ',', ($self->{cfg}{pkgs}{$_}{deps} // '') for @pkgs;
+       @pkgs = uniq @pkgs;
+       addpkg $_, $dest for @pkgs;
+       system $self->{cfg}{vpk} => $dest;
+       write_file catfile ($dir, 'readme.txt'), $self->{cfg}{readme};
+       zip [catfile($dir, "pkg.$self->{cfg}{vpk_extension}"), catfile($dir, 'readme.txt')], catfile($dir, 'pkg.zip'), FilterName => sub { $_ = fileparse $_ }, -Level => 1;
+       open my $fh, '<', catfile $dir, 'pkg.zip';
+       remove_tree $dir;
+       [200, ['Content-Type' => 'application/zip', 'Content-Disposition' => 'attachment; filename=pkg.zip'], $fh]
+}
+
+sub makelist {
+       my ($self, $elem, $tree, $lvl, $key) = @_;
+       my $name = HTML::Element->new('span', class => 'name');
+       $name->push_content($key);
+       $elem->push_content($name) if defined $key;
+       if (ref $tree eq 'ARRAY') {
+               my $sel = HTML::Element->new('select', name => 'pkg');
+               my $opt = HTML::Element->new('option', value => '');
+               $opt->push_content('None');
+               $sel->push_content($opt);
+               for my $pkg (sort { $a->{name} cmp $b->{name} } values $tree) {
+                       my $opt = HTML::Element->new('option', value => $pkg->{pkg}, $pkg->{default} ? (selected => 'selected') : ());
+                       $opt->push_content($pkg->{name});
+                       $sel->push_content($opt);
+               }
+               $elem->push_content($sel);
+       } else {
+               my $ul = HTML::Element->new('ul');
+               for my $key ($self->{cfg}{sort}->(keys $tree)) {
+                       my $li = HTML::Element->new('li', class => "level$lvl");
+                       $self->makelist($li, $tree->{$key}, $lvl + 1, $key);
+                       $ul->push_content($li);
+               }
+               $elem->push_content($ul);
+       }
+}
+
+sub makeindex {
+       my ($self) = @_;
+       my ($pkgs, $tree) = ($self->{cfg}{pkgs}, {});
+       for (keys $pkgs) {
+               my $ref = DiveRef ($tree, split ',', $pkgs->{$_}{path});
+               $$ref = [] unless ref $$ref eq 'ARRAY';
+               push $$ref, {pkg => $_, name => $pkgs->{$_}{name}, default => $pkgs->{$_}{default}};
+       }
+       my $html = HTML::TreeBuilder->new_from_file('index.html');
+       $self->makelist(scalar $html->look_down(id => 'list'), $tree, 1);
+       my $ret = $html->as_HTML('', ' ');
+       utf8::encode($ret);
+       [200, ['Content-Type' => 'text/html;charset=utf-8'], [$ret]]
+}
+
+sub call{
+       my ($self, $env) = @_;
+       my $req = Plack::Request->new($env);
+       return $self->makepkg($req->param('pkg')) if $req->path eq '/makepkg';
+       $self->makeindex;
+}
+
+1;
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+App::Web::VPKBuilder - Mix & match Source engine game mods
+
+=head1 SYNOPSIS
+
+  use Plack::Builder;
+  use App::Web::VPKBuilder;
+  builder {
+    enable ...;
+    enable ...;
+    App::Web::VPKBuilder->new->to_app
+  }
+
+=head1 DESCRIPTION
+
+App::Web::VPKBuilder is a simple web service for building Source engine game VPK packages. It presents a list of mods sorted into (sub)categories. The user can choose a mod from each category and will get a VPK containing all of the selected packages.
+
+=head1 CONFIGURATION
+
+APP::Web::VPKBuilder is configured via YAML files in the F<cfg> directory. The recommended layout is to have an F<options.yml> file with the global options, and one file for each source mod (original mod that may be split into more mods).
+
+=head2 Global options
+
+=over
+
+=item readme
+
+A string representing the contents of the readme.txt file included with the package.
+
+=item sort_order
+
+An array of strings representing the sort order of (sub)categories. (sub)categories appear in this order. (sub)categories that are not listed appear in alphabetical order after those listed.
+
+=item dir
+
+A string representing the directory in which the packages are built. Must be on the same filesystem as the package directory (F<pkg/>). Is created if it does not exist (but its parents must exist).
+
+=item vpk
+
+A string representing the program that makes a package out of a folder. Must behave like the vpk program included with Source engine games: that is, when called like C<vpk path/to/folder> it should create a file F<path/to/folder.ext>, where C<ext> is given by the next option. Defaults to 'vpk' (requires a script named vpk in the PATH).
+
+=item vpk_extension
+
+The extension of a package. Defaults to C<vpk>
+
+=back
+
+Example:
+
+  ---
+  readme: "Place the .vpk file in your custom directory (<steam root>/SteamApps/common/Team Fortress 2/tf/custom/)"
+  sort_order: [Scout, Soldier, Pyro, Demoman, Heavy, Engineer, Medic, Sniper, Spy, Sounds, Model]
+  dir: work
+  vpk: ./vpk
+  vpk_extension: vpk
+
+=head2 Mods
+
+Each source mod is composed of one or more directories (mods) in the F<pkg/> directory and a config file. Each config file should contain a hash named C<pkgs>. For each directory the hash should contain an entry with the directory name as key. Mod directory names may only contain the characters C<a-zA-Z0-9_->.
+
+Mod options:
+
+=over
+
+=item name
+
+A string representing the (human readable) name of the mod.
+
+=item path
+
+A comma-delimited string of the form C<category,subcategory,subcategory,...,item>. There can be any number of subcategories, but the default stylesheet is made for two-element paths (C<category,item>).
+
+If multiple mods have the same path, the user will be allowed to choose at most one of them.
+
+=item default
+
+A boolean which, if true, marks this mod as the default mod for its path.
+
+=item deps
+
+A comma-delimited string representing a list of mods that must be included in the final package if this mod is included. The pkgs hash need not contain an entry for the dependencies.
+
+For example, if two mods share a large part of their contents, then the shared part could be split into a third mod, and both of the original mods should depend on it. This third mod should not be included in the hash, as it shouldn't need to be manually selected by the user.
+
+=back
+
+Example:
+
+  ---
+  pkgs:
+    mymod-basher:
+      name: MyMod
+      path: "Scout,Boston Basher"
+      default: true
+      deps: mymod-base
+    mymod-sandman:
+      name: MyMod
+      path: "Scout,Sandman"
+      default: true
+      deps: mymod-base
+
+
+=head1 TODO
+
+For 0.001:
+* Tests
+* More/Clearer documentation
+* Nicer user interface
+
+=head1 AUTHOR
+
+Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2014 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.18.2 or,
+at your option, any later version of Perl 5 you may have available.
+
+
+=cut
diff --git a/static/index.css b/static/index.css
new file mode 100644 (file)
index 0000000..2ce6ae9
--- /dev/null
@@ -0,0 +1,28 @@
+body {
+       background-color: #272B30;
+       color: #CBCBCB;
+       padding-left: 1em;
+}
+
+ul {
+       list-style-type: none;
+}
+
+.level1 > span.name {
+       font-size: 150%;
+}
+
+.level2 > span.name {
+       font-weight: bold;
+       display: block;
+}
+
+.level2 {
+       display: inline-block;
+       margin: 0.5em;
+       width: 18em;
+}
+
+#list > ul {
+       padding-left: 0;
+}
diff --git a/t/App-Web-VPKBuilder.t b/t/App-Web-VPKBuilder.t
new file mode 100644 (file)
index 0000000..ed53722
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use Test::More tests => 1;
+BEGIN { use_ok('App::Web::VPKBuilder') };
This page took 0.019501 seconds and 4 git commands to generate.