#!/usr/bin/perl -w

# Handles bags/lists ("a bunch of") of brushes/objects/whatever for 3D Level
# design.
# Although it was written for Thief and DromEd I had more in my mind. If you
# stick to certain rules (11 faces max for circles, use Thief; etc) you can
# build Thief levels, if you dont, you can generate anything you want. You wont
# be able to use it without writing your own write2File routine, though.

# # usage:
# use Bunch;
#
# $bunch_o_stuff = Cowgen::Bunch->create("NAME");       # empty bunch
# $brush = $bunch->addBrush (type,kind,x,y,z,w,h,l,t);  # add something
# $bunch->write2File($file)                             # write to file
# undef $bunch;                         # free memory, normaly not needed

# ANNOTATIONS: see doc/notes.txt

# (c) Copyright by Tels 1999-2000. All Rights Reserved. Do NOT distribute.
# Updates and support on bloodgate.com/thief
# Thanx to Doug Church at TLG

# todo: extract group for objects, fromBin for objects

# older revision history in doc/history
# 2000-01-05 v0.2.0 Tels:
#                     added: ObjList, purgeGroups
#                     fixed: rotateByFacing now support top(), bottom()
#                            addLight takes now bright value or lighthash
# 2000-01-31 0.2.2 Tels:
#                     added: thingieDimensions, use strict 'vars'
#                            speedup of thingieeyId, removed objectById
#                            use constant
#                     fixed: write2file => stuff2vbr, removed eax stuff
#                            addRoom params
#                            asText/fromText in Text.pm
# 2000-06-07 0.2.3 Tels:
#                     added: unrotateCubes combineCubes optimize setFaceAlign
#                            getObjList applyTextures toEnd toBegin compactTime
#                            brushType brush (and virtual brushes for pyramid)
#                            delete deleteAll iterate
#                            r,g,b lights
#                     fixed: quite some bux in fromBin, reconstruct objects
#                            rotate didnt return $self for all angles == 0
#                            dimensions doesnt count deleted
#                            no rooms around deleted brushes
#                            reports in Report.pm,
#                            deleted rooms, decoding links
#                            number2bin: 0 or 1 are cube
#                            area names in fromBin/stuff2vbr
# 2000-12-27 0.2.4 Tels:
#                     fixed: some small fixed from wtm

package Cowgen::Bunch;
require Exporter;
@ISA     = qw(Exporter);
@EXPORT  = qw(
           MROOM MNOROOM MFLOW MNOFLOW ALIGN_BY_SIDES ALIGN_BY_VERTICES
           ALIGN_BY_BRUSH ALIGN_BY_NORMAL
           mroom mnoroom mflow mnoflow align_by_sides align_by_vertices
           align_by_brush align_by_normal
           );
#@EXPORT_OK = qw();

#doc: create destroy debug name
#doc: clone merge stuff2vbr mergeAligned
#doc: translate moveTo rotateAll rotate
#doc: setFaces getFaces setDefTexture getDefTexture
#doc: numBrushes numDelBrushes dimensions rotateByFacing
#doc: brush addBrush setTexture addBrushes setUVScaleRot
#doc: addRoom autoRoom applyTextures
#doc: addArea autoArea
#doc: addFlow autoFlow addObject useGamSys
#doc: addLight
#doc: numFlows numObjects numRooms numGroups numAreas numLights
#doc: scaleTexture changeTexture
#doc: firstBrush brushDimensions brushTextures brushByNr
#doc: setProperty getProperty
#doc: setDefaultAlign getDefaultAlign
#doc: extractGroup purgeGroups
#doc: thingieDimensions
#doc: unrotateCubes combineCubes optimize alignTextures
#doc: getObjList
#doc: toEnd toBegin compactTime iterate delete

use Cowgen::Vbr;
use Cowgen::Dromed;
use Cowgen::ObjList;
use Cowgen::Gamfile;
use Cowgen::Text;
use strict 'vars';

my $count = "000";
my $VERSION = 0.24;

use constant MROOM => 0x100;
use constant MNOROOM => 0x200;
use constant MFLOW => 0x100;
use constant MNOFLOW => 0x200;

# 00 = default from bunch
use constant ALIGN_BY_VERTICES => 0x800;
use constant ALIGN_BY_SIDES => 0x400;
use constant ALIGN_BY_BRUSH => 0x1;
use constant ALIGN_BY_NORMAL => 0x0;

# old, slower variants
sub mroom { 0x100; }
sub mnoroom { 0x200; }
sub mflow { 0x100; }
sub mnoflow { 0x200; }

# 00 = default from bunch
sub align_by_vertices { 0x800; }
sub align_by_sides { 0x400; }
sub align_by_brush { 0x1; }
sub align_by_normal { 0x0; }

sub create
  {
  # create a bunch
  # parameters: name, from (textfilename or reference to vbr object), gf (ref to gamfile object)
  my $c = shift; my $n = shift || "unnamed##num##";
  my $nr = shift; my $gf = shift;
  my $s = {}; bless $s, $c; $s->init($n);
  # reference to text string
  $s->fromText($nr) if ((defined $nr) && (ref ($nr) eq 'SCALAR'));
  # ref to vbr
  $s->fromBin($nr,$gf) if ((defined $nr) && (ref ($nr) eq 'Cowgen::Vbr'));
  return $s;
  }

sub init
  {
  my $s = shift;
  my $n = shift;
  $s->{time} = 1; $s->{faces} = 6;
  $s->{brushes} = 0; $s->{rooms} = 0; $s->{areas} = 0;
  $s->{flows} = 0; $s->{lights} = 0;
  $s->{radd} = 0.5; # growth constant for rooms
  $s->{deftxt} = -1; $s->{name} = $n;
  $s->{defalign} = ALIGN_BY_VERTICES;
  $s->{groups} = 0; $s->addGroup("default",0);
  $s->{rlist} = []; $s->{blist} = [];
  $s->{olist} = Cowgen::ObjList->create();
  $s->{flist} = []; $s->{alist} = [];
  $s->{llist} = [];
  $s->{name} =~ s/##num##/"_".$count++/eg;
  $s->{creationtime} = time;
  $s->{ids} = {};
  }

sub debug
  {
  # turn verbose/debug on/off
  # parameters: switch (on, off, default on)
  my $self = shift;
  my $d = shift; $d = on() if (!defined $d);
  print "Debug ";
  if ($d == on())
    {
    print "on"; $self->{debug} = 1;
    }
  else
    {
    print "off"; $self->{debug} = 0;
    }
  print " for '",$self->{name},"'\n";
  }

sub name { my $self = shift; return $self->{name}; }

sub DESTROY
  {
  # no further cleanup needed, print name of destroyed bunch to
  # see if we accidentily leave bunches lying around
  my $self = shift;
  print "Destroyed '",$self->{name},"'\n" if ($self->{debug});
  }

sub addBrush
  {
  # add a brush to the bunch
  # parameter: type (air/water etc), kind (cube/cylinder etc), x,y,z, w,l,h, texture,
  # rx,ry,rz, textureface1, textureface2 (etc)
  my $self = shift;
  my $time = $self->{time}; $time ++; $self->{time} = $time;
  $self->{brushes}++;
  my $brush = {};
  $brush->{time} = $time; $brush->{id} = $time;
  $self->{ids}->{$time} = $brush;
  $brush->{group} = 1; # set default group
  # room/flow: 0 = auto, 1 = always, 2 = never
  $brush->{flags} = ($_[0] & 0xf00);
  $brush->{flags} = ($brush->{flags} | $self->{defalign} )
   if (($brush->{flags} & 0xc00) == 0);
  $brush->{type} = ($_[0] & 0xFF); # solid, water etc
  $brush->{kind} = $_[1] & 0xff; # cube wegde etc
  my $sides = $_[1] >> 12;
  $sides = $self->getFaces() if (($sides < 3) || ($sides > 12));
  $brush->{x} = $_[2] || 0;  $brush->{y} = $_[3] || 0;
  $brush->{z} = $_[4] || 0;
  $brush->{w} = abs($_[5] || 16); $brush->{l} = abs($_[6] || 16);
  $brush->{h} = abs($_[7] || 16);
  print "Error! Brush too big! id = $time w = $_[5]\n" if $_[5] > 65000;
  print "Error! Brush too big! id = $time l = $_[6]\n" if $_[6] > 65000;
  print "Error! Brush too big! id = $time h = $_[7]\n" if $_[7] > 65000;
  $brush->{texture} = $_[8] || $self->{deftxt};
  $brush->{rx} = $_[9] || 0; $brush->{ry} = $_[10] || 0;
  $brush->{rz} = $_[11] || 0;
  # some defaults for vpyramid
  if ($brush->{kind} >= 0x80)
    {
    $brush->{centerw} = 0; $brush->{centerl} = 0;
    $brush->{centerx} = 0; $brush->{centery} = 0;
    $sides = 6;
    }
  $brush->{faces} = genFaces($self,$brush->{kind},$sides, $brush->{texture},
    $_[12],$_[13],$_[14],$_[15],$_[16],$_[17],$_[18],$_[19],$_[20],$_[21]);
  push (@{$self->{blist}},$brush);
  $time;
  }

sub brush
  {
  # Add a brush. Unlike addBrush, flags cannot be merged with type/kind, but must be
  # separated in 'flags', and the sides must be in 'sides'.
  # named parameters: type (air/water etc), kind (cube/cylinder etc), flags,
  # texture, rx,ry,rz, sides, centerx, centery, centerw, centerl (all for vpyramid only)
  # texturelist (ref to list of textures for all possible sides; missing will be texture),
  my $self = shift;
  my ($x,$y,$z,$w,$l,$h,$p) = @_;
  $p = {} if (!defined $p);
  my $time = $self->{time}; $time ++; $self->{time} = $time;
  $self->{brushes}++;
  my $brush = {};
  $brush->{time} = $time; $brush->{id} = $time;
  $self->{ids}->{$time} = $brush;
  $brush->{group} = 1; # set default group
  # room/flow: 0 = auto, 1 = always, 2 = never
  $brush->{type} = $p->{type} || solid();
  $brush->{kind} = $p->{kind} || cube();
  $brush->{flags} = $p->{flags} || 0;
  $brush->{flags} = ($brush->{flags} | $self->{defalign} )
   if (($brush->{flags} & 0xc00) == 0);
  my $sides = $p->{sides} || 0;
  $sides = $self->getFaces() if (($sides < 3) || ($sides > 12));
  $brush->{x} = $x || 0;  $brush->{y} = $y || 0; $brush->{z} = $z || 0;
  $brush->{w} = abs($w || 16); $brush->{l} = abs($l || 16);
  $brush->{h} = abs($h || 16);
  print "Warning! Brush to big! id = $time w = $w\n" if $w > 65000;
  print "Warning! Brush to big! id = $time l = $l\n" if $l > 65000;
  print "Warning! Brush to big! id = $time h = $z\n" if $z > 65000;
  $brush->{texture} = $p->{texture} || $self->{deftxt};
  $brush->{rx} = $p->{rx} || 0; $brush->{ry} = $p->{ry} || 0;
  $brush->{rz} = $p->{rz} || 0;
  if ($brush->{kind} >= 0x80)
    {
    # virtual brush which will be composed from one or more "basic" brushes later on
    $sides = 6;
    $brush->{centerx} = $p->{centerx} || 0;
    $brush->{centery} = $p->{centery} || 0;
    $brush->{centerw} = $p->{centerw} || 0;
    $brush->{centerl} = $p->{centerl} || 0;
    }
  my $textures = $p->{texturelist} || [];
  $brush->{faces} = genFaces($self,$brush->{kind},$sides, $brush->{texture},@$textures );
  push (@{$self->{blist}},$brush);
  $time;
  }

sub addLight
  {
  # add a brush-light (omni/spot) to the bunch
  # parameter: kind (spot(); omni()), x,y,z, l
  # (light value  or light hash containing
  # hue, saturation, bright; if undefined, nothing will be created)
  # rx, ry, rz
  my $self = shift;
  return if (!defined $_[4]); # ommit if not defined
  my $time = $self->{time}; $time ++; $self->{time} = $time;
  $self->{lights}++;
  my $light = {};
  $light->{time} = $time; $light->{id} = $time;
  $self->{ids}->{$time} = $light;
  $light->{group} = 1; # set default group
  $light->{type} = $_[0] & 0xff; # spot(), omni()
  $light->{x} = $_[1] || 0;  $light->{y} = $_[2] || 0;
  $light->{z} = $_[3] || 0;
  my $l = $_[4];
  if (ref($l) eq "HASH")
    {
    $light->{h} = $l->{hue}; $light->{s} = $l->{saturation};
    $light->{b} = abs($l->{bright}||0);
    $light->{red} = $l->{red} || $l->{r}; $light->{green} = $l->{green} || $l->{g};
    $light->{blue} = $l->{blue} || $l->{b};
    }
  else
    {
    $light->{b} = abs($l);
    }
  $light->{h} = 0 if (!defined $light->{h});
  $light->{s} = 1.0 if (!defined $light->{s});
  $light->{h} = 1.0 if ($light->{h} > 1.0);
  $light->{s} = 1.0 if ($light->{s} > 1.0);
  $light->{rx} = $_[7] || 0; $light->{ry} = $_[8] || 0;
  $light->{rz} = $_[9] || 0;

  #jds; assign defaults
  $light->{red} = 0 if (!defined $light->{red});
  $light->{green} = 0 if (!defined $light->{green});
  $light->{blue} = 0 if (!defined $light->{blue});
  $light->{red} = range($light->{red},0,255);
  $light->{green} = range($light->{green},0,255);
  $light->{blue} = range($light->{blue},0,255);
  push (@{$self->{llist}},$light);
  $time;
  }

sub numFaces
  {
  # return number of faces for a given brush from type/kind and number of faces
  # in base
  my ($type,$faces) = @_; $type = $type & 0xFF;
  return 6 if ($type == cube());
  return 5 if (($type == wedge()) || ($type == prisma()));
  return ($faces+2) if ($type == cylinder());
  return ($faces+1) if (($type == pyramid()) || ($type == apexpyramid()));
  return 12 if ($type == dodecahedron());
  return 6;
  }

sub genFaces
  {
  # internal use only - it creates for a brush the list of the faces
  # [10/25/99] jds: fixed: use "-1" as default instead of $text so that
  #                        faces defaults to whatever brushes deftxt is
  # accept hash ref instead of txt num
  my $self = shift;
  my $type = shift || 0;
  my $numfaces = shift || $self->{faces};
  my $default = shift || -1;
  my $nfaces = numFaces($type,$numfaces);
  my ($f,$t);
  my $i = 0; my $faces = [];
  $default = -1 if (ref $default ne "HASH"); # if not a hash, use -1 as default
  while ($i < $nfaces)
    {
    # u,v - trans, r = rot, t = texture, s = scale, alignment
    $f = { u => 0, v => 0, r => 0, a => 0, t => $default, s => 16};
    $t = shift || $default;
    if (ref $t eq "HASH")
      {
      foreach (keys %{$t}) {$f->{$_} = $t->{$_}; } # copy
      }
    else { $f->{t} = $t; }
    push (@$faces,$f); $i++;
    }
  $faces; # return reference
  }

sub addRoom
  {
  # add a sound room to the bunch
  # parameters:
  # x,y,z,w,h,l,h,objectid (default: 1)
  my $self = shift;
  my $time = $self->{time}; $time ++; $self->{time} = $time;
  $self->{rooms}++;
  my $room = {};
  $room->{time} = $time; $room->{id} = $time;
  $self->{ids}->{$time} = $room;
  $room->{group} = 1; # set default group
  $room->{x} = shift || 0;  $room->{y} = shift || 0; $room->{z} = shift || 0;
  $room->{w} = abs(shift || 16); $room->{l} = abs(shift || 16);
  $room->{h} = abs(shift || 16);
  $room->{eax} = abs(shift || 1);
  $room->{grav} = abs(shift || 0);
  $room->{rx} = shift || 0; $room->{ry} = shift || 0; $room->{rz} = shift || 0;
  $room->{oid} = shift || 1;
  push (@{$self->{rlist}},$room);
  $time;
  }

sub addFlow
  {
  # add a flow brush to the bunch
  my $self = shift;
  my $time = $self->{time}; $time ++; $self->{time} = $time;
  $self->{flows}++;
  my $flow = {};
  $flow->{time} = $time; $flow->{id} = $time;
  $self->{ids}->{$time} = $flow;
  $flow->{group} = 1; # set default group
  $flow->{x} = $_[0] || 0;  $flow->{y} = $_[1] || 0;
  $flow->{z} = $_[2] || 0;
  $flow->{w} = abs($_[3] || 16); $flow->{l} = abs($_[4] || 16);
  $flow->{h} = abs($_[5] || 16);
  $flow->{texture} = $_[6]; #
  $flow->{rx} = $_[7] || 0; $flow->{ry} = $_[8] || 0;
  $flow->{rz} = $_[9] || 0;
  $flow->{groupid} = $_[10] || 1;
  push (@{$self->{flist}},$flow);
  $time;
  }

sub addObject
  {
  # add an object to the bunch based on archetype id
  # parameters: x,y,z, w, l, h, namedparameters (base, name, rx,ry,rz etc)
  my $s = shift;
  return $s->{olist}->createObject(@_);
  }

sub delete
  {
  # delete a brush/object/flow/room/area by its time/id
  # parameter: id (you get when creating something)
  my $s = shift;
  my $id = shift || return;
  my $t = $s->{ids}->{$id};
  if (defined $t)
    {
    $t->{deleted} = 1; return 1;
    }
  return $s->{olist}->delete($id); # try to find in list of objects
  }

sub deleteAll
  {
  # delete all brushes/objects/flows/rooms/areas/lights by giving a list of
  # stings like deleteAll("brushes", "objects")
  # does not work for objects yet
  # is slightly faster and frees memory than using iterate() with set delete
  # but removes the things permanently (no undo!)
  # parameter: list of things to kill
  my $s = shift;
  my $list = { #objects => 'olist',
     flows => 'flist', areas => 'alist',
     brushes => 'blist', rooms => 'rlist', lights => 'llist',
    };
  my $l;
  foreach (@_)
    {
    $l = $list->{$_};
    next if (!defined $l);
    #debug: print "deleting all $_ ($l) $s->{$_} $s->{$l}\n";
    undef $s->{$l}; $s->{$_} = 0;
    }
  }

sub getObjList
  {
  # return ref to object list container of bunch
  my $s = shift;
  return $s->{olist};
  }

sub getProperty
  {
  # get a property of an object
  # parameters: objectid, propertyname
  my $s = shift || return;
  return $s->{olist}->getProperty(@_);
  }

sub setProperty
  {
  # set a property of an object
  # parameters: objectid, propertyname, values
  my $s = shift || return;
  return $s->{olist}->setProperty(@_);
  }

sub setProperties
  {
  # set properties of an object
  # parameters: objectid, has (propertiename => value)
  my $s = shift || return;
  return $s->{olist}->setProperties(@_);
  }

sub addLink
  {
  # add a link from object to object (incl backlink)
  # parameters: from, to, fl, datalist (array)
  my $s = shift;
  return $s->{olist}->addLink(@_);
  }

sub addArea
  {
  # add an area brush
  my $self = shift;
  my $time = $self->{time}; $time ++; $self->{time} = $time;
  $self->{areas}++;
  my $area = {};
  $area->{time} = $time; $area->{id} = $time;
  $area->{group} = 1; # set default group
  $area->{x} = $_[0] || 0;  $area->{y} = $_[1] || 0;
  $area->{z} = $_[2] || 0;
  $area->{w} = abs($_[3] || 16); $area->{l} = abs($_[4] || 16);
  $area->{h} = abs($_[5] || 16);
  $area->{enabled} = $_[6] || 0; # default disable
  $area->{low} = $_[7] || 0;
  $area->{high} = $_[8] || 1;
  $area->{name} = $_[9] || "unnamedArea##num##";
  $area->{name} =~ s/##num##/"_".$self->{areas}/eg;
  $area->{rx} = $_[10] || 0; $area->{ry} = $_[11] || 0;
  $area->{rz} = $_[12] || 0;
  push (@{$self->{alist}},$area);
  $time;
  }

sub addGroup
  {
  # internal: add a group name/id mapping
  # parameter: name, newid (optional)
  my $self = shift;
  $self->{groups} ++;
  my $g = {};
  $g->{name} = shift || "unnamedGroup##num##";
  $g->{name} =~ s/##num##/"_".$self->{groups}/eg;
  # make ID unique (check for existing IDs not implemented yet)
  $g->{id} = shift || $self->{groups};
  push (@{$self->{glist}},$g);
  return $g; # group
  }

sub brushType
  {
  # this needs an brush ID (actually its time value) as first parameter and returns
  # its type (water/air/ etc). If you a give second parameter, you set its kind
  # to this (water/air etc).
  # parameters: time, type
  my $s = shift;
  my $t = shift || return;
  my $type = shift;
  my $b = $s->{ids}->{$t} || return;
  $b->{type} = $type if (defined $type);
  return $b->{type};
  }

sub setTexture
  {
  # this needs an brush ID (actually its time value) as first parameter. You
  # get one when you call addBrush, so save it if you want to modify the
  # textures on the brush faces numbers are top(), bottom(), etc
  # parameters: facenr, texture (can be hash)
  my $self = shift;
  my $brush = $self->thingieById(shift);
  return if (!defined $brush);
  my ($t,$fl,$face,$text,$i,$c,@faces);
  $c = (scalar @_) >> 1;
  @faces = @{$brush->{faces}};
  my $nfaces = scalar @faces;
  for ($i=0; $i<$c; $i++)
    {
    $face = shift;
    $t = shift || -1;
    if ($face == defaultFace())
      {
      $brush->{texture} = $t;
      }
    else
      {
      return if (($face > $nfaces) || ($face < -2)); # wrong face
      $face-- if ($face > 0); # not for top(), bottom()
      $fl = $faces[$face];
      if (ref $t eq "HASH")
        {
        foreach (keys %{$t}) {$fl->{$_} = $t->{$_}; } # copy
        }
      else { $fl->{t} = $t; }
      }
    }
  }

sub setUVScaleRot
  {
  # this needs an brush ID (actually its time value) as first parameter. You
  # get one when you call addBrush, so save it if you want to modify the
  # textures on the brush faces numbers are top(), bottom(), etc
  # give it list of: facenr,u,v,scale,rot
  my $self = shift;
  my $brush = $self->thingieById(shift);
  return if (!defined $brush);
  my ($r,$fl,$face,$text,$i,$c,@faces,$u,$v,$s);
  $c = int((scalar @_) / 5);
  @faces = @{$brush->{faces}};
  my $nfaces = scalar @faces;
  for ($i=0; $i<$c; $i++)
    {
    $face = shift || 1;
    $u = shift || 0; $v = shift || 0; $s = shift || 16; $r = shift || 0;
    return if (($face > $nfaces) || ($face < -2)); # wrong face
    $face-- if ($face > 0); # not for top(), bottom()
    $fl = $faces[$face]; $fl->{u} = $u; $fl->{v} = $v; $fl->{s} = $s; $fl->{r} = $r;
    }
  }

sub setFaceRot
  {
  # this needs an brush ID (actually its time value) as first parameter. You
  # get one when you call addBrush, so save it if you want to modify the
  # textures on the brush faces numbers are top(), bottom(), etc
  my $self = shift;
  my $brush = $self->thingieById(shift);
  return if (!defined $brush);

  my $c = (scalar @_) >> 1;
  my @faces = @{$brush->{faces}};
  my $nfaces = scalar @faces;
  my ($fl,$face,$rot,$i);
  for ($i=0; $i<$c; $i++)
    {
    # jds; defaultFace() is the default texture face
    $face = shift; $rot = shift;
    return if (($face > $nfaces) || ($face < -2)); # wrong face
    $face-- if ($face > 0); # not for top(), bottom()
    $fl = $faces[$face]; $fl->{r} = $rot;
    }
  }

sub setFaceAlign
  {
  # set face texture alignment method (ALIGN_BY_BRUSH or ALIGN_BY_NORMAL)
  # this needs an brush ID (actually its time value) as first parameter. You
  # get one when you call addBrush, so save it if you want to modify the
  # textures on the brush faces numbers are top(), bottom(), etc
  my $self = shift;
  my $brush = $self->thingieById(shift);
  return if (!defined $brush);

  my $c = (scalar @_) >> 1;
  my @faces = @{$brush->{faces}};
  my $nfaces = scalar @faces;
  my ($fl,$face,$a,$i);
  for ($i=0; $i<$c; $i++)
    {
    $face = shift; $a = shift;
    return if (($face > $nfaces) || ($face < -2)); # wrong face
    $face-- if ($face > 0); # not for top(), bottom()
    $fl = $faces[$face]; $fl->{a} = $a;
    }
  }

sub setFaceScale
  {
  # this needs an brush ID (actually its time value) as first parameter. You
  # get one when you call addBrush, so save it if you want to modify the
  # textures on the brush faces numbers are top(), bottom(), etc
  my $self = shift;
  my $brush = $self->thingieById(shift);
  return if (!defined $brush);
  my $c = (scalar @_) >> 1;
  my @faces = @{$brush->{faces}};
  my $nfaces = scalar @faces;
  my ($fl,$face,$scale,$i);
  for ($i=0; $i<$c; $i++)
    {
    $face = shift; $scale = shift;
    return if (($face > $nfaces) || ($face < -2)); # wrong face
    $face-- if ($face > 0); # not for top(), bottom()
    $fl = $faces[$face]; $fl->{s} = $scale;
    }
  }

sub setFaceU
  {
  # this needs an brush ID (actually its time value) as first parameter. You
  # get one when you call addBrush, so save it if you want to modify the
  # textures on the brush faces numbers are top(), bottom(), etc
  my $self = shift;
  my $brush = $self->thingieById(shift);
  return if (!defined $brush);
  my $c = (scalar @_) >> 1;
  my @faces = @{$brush->{faces}};
  my $nfaces = scalar @faces;
  my ($fl,$face,$u,$i);
  for ($i=0; $i<$c; $i++)
    {
    $face = shift; $u = shift;
    return if (($face > $nfaces) || ($face < -2)); # wrong face
    $face-- if ($face > 0); # not for top(), bottom()
    $fl = $faces[$face]; $fl->{u} = $u;
    }
  }

sub setFaceV
  {
  # this needs an brush ID (actually its time value) as first parameter. You
  # get one when you call addBrush, so save it if you want to modify the
  # textures on the brush faces numbers are top(), bottom(), etc
  my $self = shift;
  my $brush = $self->thingieById(shift);
  return if (!defined $brush);
  my $c = (scalar @_) >> 1;
  my @faces = @{$brush->{faces}};
  my $nfaces = scalar @faces;
  my ($fl,$face,$v,$i);
  for ($i=0; $i<$c; $i++)
    {
    $face = shift; $v = shift;
    return if (($face > $nfaces) || ($face < -2)); # wrong face
    $face-- if ($face > 0); # not for top(), bottom()
    $fl = $faces[$face]; $fl->{v} = $v;
    }
  }

sub thingieById
  {
  # return a thingie reference from the unique ID value (not time!) for
  # internal usage only, since you give time values when refering to them
  my $self = shift;
  my $id = shift || 0;
  return $self->{ids}->{$id};
  }

sub brushById
  {
  # return an brush reference from the brush unique ID value for internal
  # usage only, since you give brush id values when refering to them
  my $self = shift;
  return $self->thingieById (shift);
  }

sub firstBrush
  {
  # give first brush based on kind/type
  my $self = shift;
  my $kind = shift || 0; my $type = shift || 0;
  my $brlist = $self->{blist};
  my ($b,$br);
  foreach $br (@$brlist)
    {
    if (($kind == -1) || ($kind == $br->{kind}))
      {
      if (($type == -1) || ($type == $br->{type}))
        {
        return $br->{id};
        }
      }
    }
  return -1;
  }

sub brushByNr
  {
  # return id value of the xth brush (maybe slow!)
  my $self = shift;
  my $x = shift || 0;
  my $brlist = $self->{blist};
  my ($b,$br,$k,%bbt);
  # make list of brushes by time
  foreach $br (@$brlist)
    {
    $bbt{$br->{id}} = $br;
    }
  # now make sorted list
  my @f = sort by_num keys %bbt;
  $x = -1 if ($x > scalar @f);
  $x = 0 if ($x < 0);
  return $bbt{$f[$x]}->{id};
  }

sub by_num
  {
  $a <=> $b;
  }

sub brushDimensions
  {
  # return x,y,z and w,l,h, faces of given brush id
  my $self = shift;
  return $self->thingieDimensions(shift);
  }

sub thingieDimensions
  {
  # return x,y,z and w,l,h, faces of given thingy
  # parameter: time
  my $self = shift;
  my $br = $self->thingieById (shift);
  return (-1,-1,-1,-1,-1,-1,-1) if (!defined $br);
  return ($br->{x},$br->{y},$br->{z},$br->{w},$br->{l},$br->{h},scalar @{$br->{faces}});
  }

sub brushTextures
  {
  # return texture numbers of faces of given brush id
  my $self = shift;
  my $br = $self->thingieById (shift);
  return (-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1) if (!defined $br);
  my $f = $br->{faces};
  my (@t,$fi);
  foreach $fi (@$f) { push (@t,$fi->{t}); }
  return @t;
  }

sub brush2Number
  {
  # from a brush kind make the magic number dromed expects in the VBR
  my ($t,$flags,$f) = @_;
  my $align = 0;
  $align = 1 if (($flags & ALIGN_BY_SIDES) != 0);
  $align = 0 if (($flags & ALIGN_BY_VERTICES) != 0);
  return 1 if ($t == cube());
  return 7 if ($t == wedge());
  return (2+$align)*256+$f-5 if ($t == cylinder());
  return 6 if ($t == dodecahedron());
  return (4+$align)*256+$f-4 if ($t == pyramid());
  return (2+$align)*256 if ($t == prisma());             # really is 3-sided cirlce :)
  return (6+$align)*256+$f-4 if ($t == apexpyramid());
  }

sub number2Brush
  {
  # from a BRLIST brush number make its kind and align
  my ($nr,$br) = @_;
  my ($align,$n);
  $br->{kind} = cube() if ($nr <= 1);
  $br->{kind} = wedge() if ($nr == 7);
  $br->{kind} = dodecahedron() if ($nr == 6);
  if ($nr > 256)
    {
    $n = ($nr >> 8) & 0x6; $align = ($nr >> 8) & 0x1;
    $br->{flags} = $br->{flags} | ALIGN_BY_SIDES if ($align == 1);
    $br->{flags} = $br->{flags} | ALIGN_BY_VERTICES if ($align == 0);
    $br->{kind} = cylinder() if ($n == 2);
    $br->{kind} = pyramid() if ($n == 4);
    $br->{kind} = apexpyramid() if ($n == 6);
    }
  if (!defined $br->{kind})
    {
    print "Error: Bunch::number2Brush: Brush magic type $nr is unknown! Will use cube().\n";
    print " Encountered at: Time: $br->{time} (x,y,z)/(w,l,h) = ($br->{x},$br->{y},$br->{z})/";
    print "($br->{w},$br->{l},$br->{h})\n";
    $br->{kind} = cube(); # default for unknowns
    }
  }

sub fip
  {
  return $_[0] / 2; # return for packing into float variable
  }


sub rot
  {
  # changed by wtm (I dunno if the new variant is more "correct")
  #use integer;
  #my $i = shift;
  #return ($i << 13) / 45; # return number of ratation angle
  my $i = shift;
  $i*=8192;
  $i/=45;
  my $j=int($i);
  if ($i-$j>=0.5)
    {
    $j++;
    }
  return $j;
  }

sub derot
  {
  # changed by wtm (I dunno if the new variant is more "correct")
  #return int(1000*($_[0]*45)/8192)/1000; # return rotation angle
  return sprintf("%.2f", (($_[0]*45)/8192));
  }

sub defip
  {
  return $_[0]*2;
  }

sub asText
  {
  # $flags == 1 means a longer commented text file
  # parameters: $flags

  my $self = shift;
  my $flags = shift || 0;

  my $map = { blist => "Brush", olist => "Object", glist => "Group",
              flist => "Flow", alist => "Area", llist => "Light",
              rlist => "Room", faces => "Face", };

  my $dontput = { debug => 1, flows => 1, rooms => 1, ids => 1, groups => 1,
                  faces => 1, creationtime => 1, defalign => 1, deftxt => 1,
                  time => 1,
                  objects => 1, lights => 1, areas => 1, brushes => 1};
  my $txt = Cowgen::Text::asText ($self, "Bunch", 1, $dontput, $map );
  $txt .= $self->{olist}->asText();
  $txt;
  }

sub stuff2vbr
  {
  # first parameter is the VBR object, if this is undefined, a new one is
  # created on the fly and this will use "dummy.vbr". see doc/notes.txt
  # parameters: vbr, gamfile

  my $self = shift;
  my $vbr = shift;
  my $gf = shift;
  my $flags = 0;
  $vbr = Cowgen::Vbr->create() if (!defined $vbr); # if not defined, make new
  $gf = Cowgen::Gamfile->create() if (!defined $gf);
  my ($hr,$brush,$facelist,$key,$bkey,$fkey,$br,@bkeys,$fl,@fkeys,$fk,$fbuf,$r);

  print "Stuffing meself into VBR:\n" if ($self->{debug});
  print "'",$self->{name},"' contains:\n ",$self->numBrushes()," brushes, ",
   $self->numRooms()," rooms, ",
   $self->numFlows()," flows, ",$self->numObjects()," objects and ",
   $self->{lights}," lights in ",$self->{groups}," groups.\n"
   if ($self->{debug});

  my @keys = sort keys %$self;
  my $buffer = ""; my $buf ="";
  my $date = scalar localtime;
  # this really needs to be cleaned instead of copy&paste
  my $basetime = 0; # to get virtual brushes to work
  my $pyra = pyramid();
  my ($brushes,$bri);
  foreach $key (@keys)
    {
    if ($key eq "blist")
      {
      $brush = $self->{$key};
      foreach $bri (@$brush)
        {
        next if ($bri->{deleted});
        $brushes = [ $bri ]; # default: real brush
        $brushes = $self->_assemblePyramid($bri) if ($bri->{kind} >= 0x80); # virtual brush
        foreach $br (@$brushes)
          {
          $buf = pack ("SSLSCCffffffSSSSCCCCCCCCCCCCCCCCCCCCCCCCCCCCL",
           $basetime + $br->{time},$basetime + $br->{id},brush2Number($br->{kind},$br->{flags},
           scalar @{$br->{faces}}), $br->{texture},$br->{type},0,
           $br->{x},$br->{y},$br->{z},
           fip($br->{l}),fip($br->{w}),fip($br->{h}),
           rot($br->{rx}),rot($br->{ry}),rot($br->{rz}),
           0xFFFF,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
           scalar @{$br->{faces}},
           0,0,(($br->{flags} & 0xc00) >> 8)-4,$br->{group},0);
          #print "gen $basetime+$br->{id}\n";
          $fbuf = "";
          $fl = $br->{faces};
          foreach $fkey (@$fl)
            {
            $r = rot($fkey->{r});
            $r = 1 if ($fkey->{a} == ALIGN_BY_BRUSH);
            $fbuf .= pack ("SSSSS",
             $fkey->{t}, $r, $fkey->{s}, $fkey->{u}, $fkey->{v});
            }
          $basetime++;
          $buffer .= $buf . $fbuf;
          } # end for all sub brushes
        $basetime--; # correct (0 for 1 subbrush, 1 for 2 subbrushes etc)
        }
      } # brushes
    if ($key eq "rlist")
      {
      $brush = $self->{$key};
      foreach $br (@$brush)
        {
        next if ($br->{deleted});
        $buf = pack ("SSLSCCffffffSSSSCCCCCCCCCCCCCCCCCCCCCCCCCCCCL",
         $basetime+$br->{time},$basetime+$br->{time}+256,$br->{oid},
         # XXROOMXX: the number before the 0xFB is a counting number after
         # DromEd has build the room database, and before that just 0xFFFF
         # (unitialized) Could we build the room database by our selfes?? ;)
         0xFFFF,0xFB,0,
         $br->{x},$br->{y},$br->{z},
         fip($br->{l}),fip($br->{w}),fip($br->{h}),
         rot($br->{rx}),rot($br->{ry}),rot($br->{rz}),
         0xFFFF,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
         6,
         0,0,0,$br->{group},0);
        $buffer .= $buf;
        }
      } # rooms
    if ($key eq "alist")
      {
      $brush = $self->{$key};
      $hr = {};
      foreach $br (@$brush)
        {
        next if ($br->{deleted});
        $buf = pack ("SSLSCCffffffSSSSCCCCCCCCCCCCCCCCCCCCCCCCCCCCL",
         $basetime+$br->{time},$basetime+$br->{time}+256,0,
         $br->{enabled},0xFE,0,
         $br->{x},$br->{y},$br->{z},
         fip($br->{l}),fip($br->{w}),fip($br->{h}),
         rot($br->{rx}),rot($br->{ry}),rot($br->{rz}),
         0xFFFF,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
         6,
         0,0,0,$br->{group},0);
        $hr->{$basetime+$br->{time}} = $br->{name}; # build hotregions name list
        $buffer .= $buf;
        }
      $vbr->hotRegions($hr); # stuff area names into $vbr
      } # areas
    if ($key eq "flist")
      {
      $brush = $self->{$key};
      foreach $br (@$brush)
        {
        next if ($br->{deleted});
        $buf = pack ("SSLSCCffffffSSSSCCCCCCCCCCCCCCCCCCCCCCCCCCCCL",
         $basetime+$br->{time},$basetime+$br->{time}+256,1,
         $br->{groupid},0xFC,0,
         $br->{x},$br->{y},$br->{z},
         fip($br->{l}),fip($br->{w}),fip($br->{h}),
         rot($br->{rx}),rot($br->{ry}),rot($br->{rz}),
         0xFFFF,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
         6,
         0,0,0,$br->{group},0);
        $buffer .= $buf;
        }
      } # flows
    if ($key eq "olist")
      {
      $buffer .= $self->{olist}->asBin($vbr,$gf);
      } # objects
    if ($key eq "llist")
      {
      $brush = $self->{$key};
      foreach $br (@$brush)
        {
        next if ($br->{deleted});
        next if ((defined $br->{deleted}) && ($br->{deleted} == 1));
        $buffer .= pack ("SSLSCCffffffSSSSCCCCCCCCCCCCCCCCCCCCCCCCCCCCL",
         $basetime+$br->{time},$basetime+$br->{id},1,
         0,0xFF,0,
         $br->{x},$br->{y},$br->{z},
         $br->{b},$br->{h},$br->{s},
         rot($br->{rx}),rot($br->{ry}),rot($br->{rz}),
         0xFFFF,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
         $br->{type},
         0,0,0,$br->{group},0);
        }
      } # lights
    }
  # build multibrushnames from group list
  $hr = {}; foreach (@{$self->{glist}}) { $hr->{$_->{id}} = $_->{name} || ""; }
  $vbr->multibrushes($hr); # stuff group names into $vbr
  $vbr->putChunk("BRLIST",\$buffer,1,0);
  $vbr->delChunk("WR"); # no world rep info (portalisation)
  $vbr->delChunk("AIPATH"); # no ai path database
  $vbr->delChunk("ROOM_DB"); # no room database
  # creator and working time (in 1/1000 seconds)
  my $time = (time - $self->{creationtime})*1000;
  my $grid = 12; my $usegrid = 1; my $showgrid = 1;
  $buffer = pack ("a16a16x68LLLLLL", "Cowgen::Bunch","CoW $VERSION",
   $grid,$showgrid,0,0,$usegrid,$time);
  $vbr->putChunk("BRHEAD",\$buffer,0x09,0x7c);
  }

sub _assemblePyramid
  {
  # return list of brushes for assembled pyramid (offcenter, twocentered etc)
  my ($self,$b) = @_;
  # make absolut sizes/coords from relatice places, needs to be done here due honor
  # resizing etc of brush after creation
  # center ranges from -0.5 .. 0.5
  # width/length range from 0..-1 or 0..width/length
  #print "Assemblepyramid\n";
  my $w = $b->{w}||0; my $l = $b->{l}||0;
  my $x = $b->{x}||0; my $y = $b->{y}||0;
  my $cx = $l-rel2abs(($b->{centerx}||0)-0.5,$l,1);
  my $cy = $w-rel2abs(($b->{centery}||0)-0.5,$w,1);
  my $cw = rel2abs($b->{centerw},$w,1); my $cl = rel2abs($b->{centerl},$l,1);
  my $ll2 = $l/2;  my $ww2 = $w/2;
  return [ $b ] if (($cx == $ll2) && ($cy == $ww2) && ($cw == 0) && ($cl == 0)); # simple pyramid
  my ($x1,$y1,$x2,$y2,$w1,$w2,$w3,$l1,$l2,$l3);
  # no need to confine x1,x2 inside x +- l/2 since cx is already confined to 0..l
  my $xl = $x - $ll2 + $cx; my $yl = $y - $ww2 + $cy;
  $x1 = $xl - $cl/2; $x2 = $xl + $cl/2;
  $y1 = $yl - $cw/2; $y2 = $yl + $cw/2;
  $l1 = $cx - $cl/2; $l3 = $l - $cx - $cl/2;
  $w1 = $y1 - $y + $ww2; $w3 = - $y2 + $y + $ww2;
  $w2 = $cw; $l2 = $cl;
  #print "xl: $xl yl: $yl w: $w l: $l x: $x y: $y cw: $cw cl: $cl cx: $cx cy: $cy\n";
  my (@x,@y);
  $x[1] = $xl; $y[1] = $yl;
  $y[0] = $y[1]-$w2/2-$w1/2; $y[2] = $y[1]+$w2/2+$w3/2;
  $x[0] = $x[1]-$l2/2-$l1/2; $x[2] = $x[1]+$l2/2+$l3/2;
  my @br;
  my $a = ALIGN_BY_SIDES();
  # wedge is default
  $br[0]->[0] = { kind => apexpyramid(), rz => 270, w => $l1, l => $w1, flags => $a, };
  $br[0]->[1] = { rz => 90, w => $l1, l => $w2 };
  $br[0]->[2] = { kind => apexpyramid(), rz => 180, w => $w3, l => $l1, flags => $a };
  $br[1]->[0] = { rz => 180, w => $w1, l => $l2 };
  $br[1]->[1] = { kind => cube(), w => $w2, l => $l2 };
  $br[1]->[2] = { w => $w3, l => $l2 };
  $br[2]->[0] = { kind => apexpyramid(), w => $w1, l => $l3, flags => $a };
  $br[2]->[1] = { rz => 270, w => $l3, l => $w2 };
  $br[2]->[2] = { kind => apexpyramid(), rz => 90, w => $l3, l => $w3, , flags => $a };
  my @tx;
  # texture definitions
  $tx[0]->[0] = [ 3, 0, 1, 2, 5 ];
  $tx[0]->[1] = [ 0, 5, 2, 3, 1 ];
  $tx[0]->[2] = [ 2, 3, 0, 1, 5 ];
  $tx[1]->[0] = [ 1, 5, 3, 0, 2 ];
  $tx[1]->[1] = [ 0, 1, 2, 3, 4, 5 ]; # cube is just default
  $tx[1]->[2] = [ 3, 5, 1, 2, 0 ];
  $tx[2]->[0] = [ 0, 1, 2, 3, 5 ];
  $tx[2]->[1] = [ 2, 5, 0, 1, 3 ];
  $tx[2]->[2] = [ 1, 2, 3, 0, 5 ];
  my (@list,$brush,$ab,@c,@txts,$t);
  # debug:
  for (my $i = 0; $i < 3; $i++)
    {
    for (my $j = 0; $j < 3; $j++)
      {
      $brush = { rz => 0, ry => 0, rx => 0, w => 0, l => 0, kind => wedge(), x => $x[$i],
        y => $y[$j], flags => 0, };
      foreach (keys %$b) { $brush->{$_} = $b->{$_} if (!defined $brush->{$_}); }
      $ab = $br[$i]->[$j]; foreach (keys %$ab) { $brush->{$_} = $ab->{$_}; }
      delete $brush->{faces};
      # skip brushes that are 0 in one dimension, this enables us to skip all
      # the funky special cases while still creating minimum brush count
      if ( ($brush->{w} != 0) && ($brush->{l} != 0))
        {
        # retexturing
        $t = 0; foreach (@{$tx[$i]->[$j]})
          {
          $txts[$t] = $b->{faces}->[$_]; #print "$t,$_ => $txts[$t]\n";
         $t++;
          }
        $brush->{faces} = genFaces($self,$brush->{kind},4,$brush->{texture}, @txts);
        # roatate it
        ($brush->{x},$brush->{y},$brush->{z}) = rotatePoint ($brush->{x},$brush->{y},$brush->{z},
          $b->{x},$b->{y},$b->{z},$b->{rx},$b->{ry},$b->{rz});
        $brush->{rx} += $b->{rx}; $brush->{ry} += $b->{ry}; $brush->{rz} += $b->{rz};
        clipAngle($brush->{rx},$brush->{ry},$brush->{rz});
        push @list, $brush;
        #printHash ($brush); #debug
        }
      }
    }
  return \@list;
  }

sub fromBin
  {
  # take binary data and convert it to internal format
  # parameters: vbr, gf
  my $s = shift;
  my $vbr = shift || return;
  my $gf = shift || return;
  my $buf = $vbr->getChunk('BRLIST');
  my $hr = $vbr->hotRegions(); # get list of area id => names
  my $mbr = $vbr->multibrushes(); # get list of group ids => names
  if (!defined $buf)
    {
    print "Bunch::fromBin: Error: Cant find BRLIST\n"; return;
    }
  my (@f,$ofs,$l,$i,$faces,$br,$fi,@fa);
  $ofs = $vbr->headerSize(); $l = length($$buf);
  my %groups; # group cache
  while ($ofs < $l)
    {
    @f = unpack ("SSLSCCffffffSSSSCCCCCCCCCCCCCCCCCCCCCCCCCCCCL", substr($$buf,$ofs,76));
    $ofs += 76;
    $br = {}; # make empty thingy
    $br->{time} = $f[1]; $br->{id} = $f[0]; $br->{flags} = 0;
    $s->{time} = $f[1] if ($f[1] > $s->{time});
#    print "f $f[0] $f[1] $f[2]\n";
    $br->{x} = $f[6]; $br->{y} = $f[7]; $br->{z} = $f[8];
    $br->{l} = defip($f[9]); $br->{w} = defip($f[10]); $br->{h} = defip($f[11]);
    $br->{rx} = derot($f[12]); $br->{ry} = derot($f[13]); $br->{rz} = derot($f[14]);
    $br->{texture} = $f[3];
    $br->{group} = $f[43];
    # check if group exists and if not create it (but not group 0 = default)
    if (($f[43] != 0) && (!defined $groups{$f[43]}))
      {
      #print "group: $f[43] ",$mbr->{$f[43]} || "not defined","\n";
      $groups{$f[43]} = $s->addGroup($mbr->{$f[43]},$f[43]);
      }
    if (($f[4] >= 0) && ($f[4] < 128))
      {
      # brush
      number2Brush($f[2],$br);
      $br->{type} = $f[4];
      # read # of faces
      $faces = $f[39];
      $br->{faces} = []; # make empty faces
      for ($i = 0; $i < $faces; $i++)
        {
        @fa = unpack ("SSSSS", substr($$buf,$ofs,10));
	# derot fix by wtm
        $fi = { t => $fa[0], r => 0, s => $fa[2], u => $fa[3], v => $fa[4],
          a => ALIGN_BY_BRUSH };
        if ($fa[1] != 1)
          {
          $fi->{a} = ALIGN_BY_NORMAL;
          $fi->{r} = derot ($fa[1]);
          }
        $br->{faces}->[$i] = $fi;
        $ofs += 10;
        }
      push (@{$s->{blist}},$br);
      $s->{brushes}++;
      }
    else
     {
      if ($f[4] == 0xFD)
        {
        # dont bother to decode object info in brlist chunk
        # we reconstruct it later from metapropeties etc
        # has the advantage that it works with BRLIST
        $br->{oid} = $f[2];
        }
      if ($f[4] == 0xFC)
        {
        $br->{groupid} = $f[3];
        push (@{$s->{flist}},$br);
        $s->{flows}++;
        }
      if ($f[4] == 0xFB)
        {
        $br->{oid} = $f[2];
        #$br->{eax} = 0;
        $br->{gravity} = 100;
        push (@{$s->{rlist}},$br);
        $s->{rooms}++;
        }
      if ($f[4] == 0xFE)
        {
        delete $br->{texture}; $br->{enabled} = $f[3]; # enabled/disabled
        $br->{time} = $br->{id};
        $br->{name} = $hr->{$br->{id}} || ""; # name from hotregions
        push (@{$s->{alist}},$br);
        $s->{areas}++;
        }
      if ($f[4] == 0xFF)
        {
        $br->{b} = $f[9]; $br->{h} = $f[10]; $br->{s} = $f[11];
        delete $br->{w}; delete $br->{l};
        delete $br->{texture}; delete $br->{flags};
        $br->{type} = $f[39];
        push (@{$s->{llist}},$br);
        $s->{lights}++;
        }
      if ($f[4] < 0xFB)
        {
        print "Error: Bunch->fromBin: Unknown type f4 $f[4]\n";
        }
      }
    } # next thingy
  $s->{olist} = Cowgen::ObjList->create($vbr,$gf);
  $gf->decodeLinks ($vbr,$s->{olist});
  return $s;
  } # done fromBin

sub setFaces
  {
  # set number of faces for cylinder, pyramid, apexcorner pyramid
  # for Thief it is actually 6..11
  my $self = shift;
  my $faces = abs(shift) || 6; $faces =  1024 if ($faces < 3) or ($faces > 1024);
  $self->{faces} = $faces;
  return $faces;
  }

sub getFaces
  {
  # get number of faces for cylinder, pyramid, apexcorner pyramid
  my $self = shift; return $self->{faces};
  }

sub setDefTexture
  {
  # set default texture
  my $self = shift; $self->{deftxt} = shift || -1;
  }

sub getDefTexture
  {
  # get default texture
  my $self = shift; return $self->{deftxt};
  }

sub setDefAlign
  {
  # set default alignment of brushes (cylinders, pyramids)
  my $self = shift;
  my $align = shift;
  $self->{defalign} = ALIGN_BY_VERTICES;
  $self->{defalign} = ALIGN_BY_VERTICES if ($align == ALIGN_BY_VERTICES);
  $self->{defalign} = ALIGN_BY_SIDES if ($align == ALIGN_BY_SIDES);
  }

sub getDefAlign
  {
  # get default alignment of brushes (cylinders, pyramids)
  my $self = shift; return $self->{defalign};
  }

sub numDelBrushes
  {
  # return number of "deleted" brushes
  my $self = shift; my $brlist = $self->{blist};
  my $d = 0;
  foreach $b (@$brlist)
    {
    $d++ if ((defined $b->{deleted}) && ($b->{deleted} == 1));
    }
  return $d;
  }

sub numBrushes
  {
  # return number of brushes
  my $self = shift; return @{$self->{blist}} - $self->numDelBrushes;
  }

sub numRooms
  {
  # return number rooms
  my $self = shift; return $self->{rooms};
  }

sub numFlows
  {
  # return number of flows
  my $self = shift; return $self->{flows};
  }

sub numObjects
  {
  # return number of objects
  my $self = shift; return $self->{olist}->numObjects();
  }

sub numAreas
  {
  # return number of area brushes
  my $self = shift; return $self->{areas};
  }

sub numGroups
  {
  # return number of groups
  my $self = shift; return $self->{groups};
  }

sub numLights
  {
  # return number of brush lights
  my $self = shift; return $self->{lights};
  }

sub dimensions
  {
  # return (xc,yc,zc,w,l,h) (center/size) of bunch
  # only brushes do count here!
  # not ready yet
  my $self = shift;
  my $brlist = $self->{blist};
  my ($min,$k,$i,$j,@l,@v,$cnt);
  $k = scalar @$brlist; return (0,0,0,0,0,0) if ($k == 0);
  $b = @{$brlist}->[0];
  my ($xmin,$xmax,$ymin,$ymax,$zmin,$zmax);
  # look at first brush to get start values
  $xmin = $b->{x}; $xmax = $b->{x};
  $ymin = $b->{y}; $ymax = $b->{y};
  $zmin = $b->{z}; $zmax = $b->{z};
  # it would be possible to merge the two loops, but then again you
  # would get for the worst case (spiraling brushes outwards) get to check
  # the vertices for each brush, and the average case would be less optimal,
  # too. So we do a quick check and calc a min. dimension for the brush by
  # using the centers and then only check brushes that still stick out.
  # dont use first brush, this is already covered
  for ($i = 1; $i < $k; $i++)
    {
    # tels: fixed
    $b = @{$brlist}->[$i];
    next if $b->{deleted};
    # see if center of brush is inside min dimension cube
    $xmin = $b->{x} if ($b->{x} < $xmin); $xmax = $b->{x} if ($b->{x} > $xmax);
    $ymin = $b->{y} if ($b->{y} < $ymin); $ymax = $b->{y} if ($b->{y} > $ymax);
    $zmin = $b->{z} if ($b->{z} < $zmin); $zmax = $b->{z} if ($b->{z} > $zmax);
    }
  $k = 0; # debug
  foreach $b (@$brlist)
    {
    next if $b->{deleted};
    # calc smallest value, for min. bounding sphere of brush
    ($min) = min( $b->{w}, $b->{l}, $b->{h} );
    if (( $xmin > $b->{x} - $min ) || ( $ymin > $b->{y} - $min ) || ( $zmin > $b->{z} - $min ) ||
        ( $xmax < $b->{x}+$min ) || ( $ymax < $b->{y}+$min ) || ( $zmax < $b->{z}+$min ))
      {
      $k ++; # debug
      @v = getVertices ($b);
      # jds; remove "/3" when computing $cnt
      $cnt = (scalar @v);
      foreach ($i = 0; $i < $cnt; )
        {
      # jds; xmin, xmax
        $j = round($v[$i],100);
        $xmin = $j if ($j < $xmin); $xmax = $j if ($j > $xmax); $i++;
      # jds; ymin, ymax
        $j = round($v[$i],100);
        $ymin = $j if ($j < $ymin); $ymax = $j if ($j > $ymax); $i++;
      # jds; zmin, zmax
        $j = round($v[$i],100);
        $zmin = $j if ($j < $zmin); $zmax = $j if ($j > $zmax); $i++;
        }
      } # endif only if brush min. bounding sphere sticks out of its min dimension cube
    }
  print "Debug: dimension took $k calls to getVertices.\n" if ($self->{debug});
  # jds; width is along "y" dimension, depth is along "x" dimension.
  $l[3] = $ymax-$ymin; $l[4] = $xmax-$xmin; $l[5] = $zmax-$zmin;
  $l[0] = $l[4]/2+$xmin; $l[1] = $l[3]/2+$ymin; $l[2] = $l[5]/2+$zmin;
  # jds; round to nearest 1/100, rather than truncate.
  foreach (@l) { $_ = round($_,100); }
  return @l;
  }

# tels: global for speeds sake

# cube, or 4 sided cylinder, ALIGN_BY_SIDES
my @cube = qw(
      -0.5 -0.5 -0.5  -0.5 -0.5 0.5
      -0.5  0.5 -0.5  -0.5  0.5 0.5
       0.5 -0.5 -0.5   0.5 -0.5 0.5
       0.5  0.5 -0.5   0.5  0.5 0.5
      );
my @wedge = qw(
      -0.5 -0.5 0.5  -0.5 -0.5 -0.5
      +0.5 -0.5 0.5  +0.5 -0.5 -0.5
      +0.5 +0.5 -0.5
      -0.5 +0.5 -0.5
      );
# aligned by sides
my @pyramid = qw(
      -0.5 -0.5 -0.5   0    0   0.5
      -0.5  0.5 -0.5
       0.5 -0.5 -0.5
       0.5  0.5 -0.5
      );
my @apexpyramid = qw(
      -0.5 -0.5 -0.5  -0.5  0.5 0.5
      -0.5  0.5 -0.5
       0.5 -0.5 -0.5
       0.5  0.5 -0.5
      );
# cylinder, ALIGN_BY_VERTICES, 4 sides
# upper, lower
my @cylinder = qw(
       0.5  0.0 -0.5
      -0.5  0.0 -0.5
       0.0 -0.5 -0.5
       0.0  0.5 -0.5
       0.5  0.0 -0.5
      -0.5  0.0 -0.5
       0.0 -0.5 -0.5
       0.0  0.5 -0.5
      );

sub getVertices
  {
  # return array of rotated/transformed vertices for brush
  # not ready yet, internal usage only
  # special handling for vpyramid still missing
  my $brush = shift || return;
  my $points;
  # cube
  $points = \@cube;
  $points = \@wedge if ($brush->{kind} == wedge());
  $points = \@cylinder if (($brush->{kind} == cylinder()) && (scalar @{$brush->{faces}} == 4));
  if (($brush->{flags} & ALIGN_BY_SIDES) != 0)
    {
    $points = \@pyramid if ($brush->{kind} == pyramid());
    $points = \@apexpyramid  if ($brush->{kind} == apexpyramid());
    }
  my $cnt = scalar @$points;
  # jds; use "delta" variables for clarity.
  my ($i,$dx,$dy,$dz,@rot,@v);
  for ($i = 0; $i < $cnt; $i += 3)
    {
    # multiply by brush size (vertices are 0.5 so /2 is not necc.)
    $dx = $points->[$i] * $brush->{l};  # jds; was "w", now depth
    $dy = $points->[$i+1] * $brush->{w};    # jds; was "l", now width
    $dz = $points->[$i+2] * $brush->{h};
    # dont rotate if not necc.
    if (($brush->{rz} != 0) || ($brush->{ry} != 0) || ($brush->{rx} != 0))
       {
       ($dx,$dy,$dz) = rotatePoint ($dx,$dy,$dz,0,0,0, $brush->{rx}, $brush->{ry}, $brush->{rz});
       }
    push (@v, $dx + $brush->{x}, $dy + $brush->{y}, $dz + $brush->{z});
    }
  return @v;    # list of vertices
  }

sub cloneThingy
  {
  # internal only, make exact copy of area/brush/flow/room/object/light
  # and add it to your self
  my $self = shift;
  my $br = shift || return;
  my $type = shift || 0;
  my $g = shift || 0;
  my $gf = shift; $gf = $br->{group} if (!defined $gf); # copy only things from this group
  return if ($br->{group} != $gf); # if gf not defined, we fall trough and copy it anyway
  my $time = $self->{time}; $time ++; $self->{time} = $time;
  $self->{brushes}++ if ($type == 0);
  $self->{rooms}++ if ($type == 1);
  $self->{flows}++ if ($type == 2);
  $self->{areas}++ if ($type == 4);
  $self->{lights}++ if ($type == 5);
  my $copy = {};
  my ($k);
  foreach $k (keys %$br)
    {
    $copy->{$k} = $br->{$k} if ($k ne "faces");
    }
  $copy->{time} = $time;
  $copy->{group} = $self->{groups} if ($g == 0); # make copy of groupid or make new
  if ($type == 0)
    {
    $copy->{faces} = [];
    foreach (@{$br->{faces}}) { push @{$copy->{faces}}, copyHash($_); }
    push (@{$self->{blist}},$copy);
    }
  push (@{$self->{rlist}},$copy) if ($type == 1);
  push (@{$self->{flist}},$copy) if ($type == 2);
  push (@{$self->{alist}},$copy) if ($type == 4);
  push (@{$self->{llist}},$copy) if ($type == 5);
  $time;
  }

sub clone
  {
  # return copy of yourself (incl. groupids)
  my $self = shift;

  my $bu = Cowgen::Bunch->create("$self->{name}##num##");
  my ($b,$list);
  $list = $self->{blist}; foreach $b (@$list) { $bu->cloneThingy($b,0,1); }
  $list = $self->{rlist}; foreach $b (@$list) { $bu->cloneThingy($b,1,1); }
  $list = $self->{flist}; foreach $b (@$list) { $bu->cloneThingy($b,2,1); }
  $bu->{olist}->merge( $self->{olist} );
  $list = $self->{alist}; foreach $b (@$list) { $bu->cloneThingy($b,4,1); }
  $list = $self->{llist}; foreach $b (@$list) { $bu->cloneThingy($b,5,1); }
  $list = $self->{glist}; foreach $b (@$list) { $bu->addGroup($b->{name},$b->{id}); }
  $bu->{groups} = $self->{groups};
  return $bu;
  }

sub purgeGroups
  {
  # Set all thingies to group 1 and purge (delete) all other groups. This is
  # very usefull if you are constructing something from bunches and want to
  # combine anything into one bunch with only one group.
  my $s = shift || return;
  # purge list and make default new
  delete $s->{glist};
  $s->addGroup("default");
  # reset all
  my $t;
  foreach $t ( @{$s->{blist}}, @{$s->{rlist}}, @{$s->{flist}},
    @{$s->{alist}}, @{$s->{llist}} )
    {
    $t->{group} = 1;
    }
  $s;
  }

sub extractGroup
  {
  # return copy of all thingies belonging to group (currently objects are not
  # cloned!!)
  my $self = shift;
  my ($g) = shift || return;
  my ($bu,$b,$list);

  # walk groups, get name of first and copy over what we need
  my $gl = $self->{groups}; my $name = "";
  foreach $b (@$gl)
    {
    if ($b->{id} == $g)
      {
      $bu = Cowgen::Bunch->create($name);
      $bu->addGroup($b->{name},$g)
      }
    }
  return if (!defined $bu); # we didnt find group to copy
  $list = $self->{blist}; foreach $b (@$list) { $bu->cloneThingy($b,0,1,$g); }
  $list = $self->{rlist}; foreach $b (@$list) { $bu->cloneThingy($b,1,1,$g); }
  $list = $self->{flist}; foreach $b (@$list) { $bu->cloneThingy($b,2,1,$g); }
  # XXX (doesnt know about groups yet!)
  $bu->{olist}->merge( $self->{olist} );
  $list = $self->{alist}; foreach $b (@$list) { $bu->cloneThingy($b,4,1,$g); }
  $list = $self->{llist}; foreach $b (@$list) { $bu->cloneThingy($b,5,1,$g); }
  return $bu;
  }

sub merge
  {
  # add another bunch to yourself (if given x,y,z bunch is moved to that posiion
  # warning. this will change (translate) the merged bunch when giving x,y,z, should
  # we make a copy first?
  # parameters: bunch (optional: x,y,z)
  my $self = shift;
  my ($bu,$x,$y,$z) = @_;
  return if (!defined $bu);
  print "Merging '",$bu->{name}, "' into '",$self->{name},"'\n" if ($self->{debug});
  $bu->moveTo($x,$y,$z) if (defined $x);
  my ($b,$list);
  $self->addGroup($bu->{name}); # add another group, cloneThingy adds to this
  $list = $bu->{blist}; foreach $b (@$list) { $self->cloneThingy($b); }
  $list = $bu->{rlist}; foreach $b (@$list) { $self->cloneThingy($b,1); }
  $list = $bu->{flist}; foreach $b (@$list) { $self->cloneThingy($b,2); }
  $self->{olist}->merge( $bu->{olist} );
  $list = $bu->{alist}; foreach $b (@$list) { $self->cloneThingy($b,4); }
  $list = $bu->{llist}; foreach $b (@$list) { $self->cloneThingy($b,5); }
  }

sub align
  {
  # center yourself at a face of a cube-brush, parameter are
  # x,y,z,w,l,h of brush, and then named parameters
  # aSouth, aNorth, aEast, aWest work place it in the wall at the given faces center,
  # give aRight or aLeft to place it at the edge
  # named parameter: place (where to place, defaults aSouth(); aRight() and aLeft not
  # supported yet), location (inside = -1; ouside = 1; inface = 0)
  my $s = shift;
  my ($br,$p) = @_;
  # aSouth, aNorth, aEast, aWest work place it in the wall at the given faces center
  my ($w,$h,$l,$x,$y,$z) = $s->dimensions();
  my $place = $p->{place} || aSouth();
  $x = $x + $l/2 if ($place & aSouth() != 0);
  $x = $x - $l/2 if ($place & aNorth() != 0);
  $y = $y + $w/2 if ($place & aWest() != 0);
  $y = $y - $w/2 if ($place & aEast() != 0);
  $z = $z + $h/2 if ($place & aTop() != 0);
  $z = $z - $h/2 if ($place & aBottom() != 0);
  #$s->merge($bunch, $x, $y, $z);
  }

# transformations

sub translate
  {
  # translate (move) all thingies by some amount
  my $self = shift;
  my $tx = shift || 0; my $ty = shift || 0; my $tz = shift || 0; # shift
  my ($b);
  foreach $b ( @{$self->{blist}}, @{$self->{rlist}}, @{$self->{flist}},
   @{$self->{alist}}, @{$self->{llist}} )
    {
    $b->{x} += $tx; $b->{y} += $ty; $b->{z} += $tz;
    }
  $self->{olist}->translate($tx,$ty,$tz);
  $self;
  }

sub moveTo
  {
  # translate (move) all thingies to a specified position
  my $self = shift;
  my $x = shift || 0; my $y = shift || 0; my $z = shift || 0;
  my ($sx,$sy,$sz,$xp,$yp,$zp) = $self->dimensions();
  $self->move ($x-$xp,$y-$yp,$z-$zp);
  $self;
  }

sub rotateByFacing
  {
  # rotate yourself by facing round given center,
  # aSouth == 0, aEast = 90, aNorth 180, aWest = 270,
  # if top and bottom are given, then bunch is rotated first to to
  # top or bottem, and then to asouth etc
  my $self = shift;
  my ($x,$y,$z,$f) = @_;
  $f = 0 if (!defined $f);
  my $za;
  $self->rotate(90,0,0,$x,$y,$z) if (($f & aTop()) != 0);
  $self->rotate(180,0,0,$x,$y,$z) if (($f & aBottom()) != 0);
  $f = ($f & 0xF); # blend out top/bottom
  $za = turnByFacingCcw($f,0);
  $self->rotate(0,0,$za,$x,$y,$z) if ($za != 0);
  $self;
  }

sub rotate
  {
  # rotate all thingies around a single pivot point
  # parameter: ax (angle),ay,az, rx (pivot point),ry,rz
  my $self = shift;
  my $ax = shift || 0; my $ay = shift || 0; my $az = shift || 0; # angle
  return $self if (($ax == 0) && ($ay == 0) && ($az == 0));
  my $rx = shift || 0; my $ry = shift || 0; my $rz = shift || 0; # center
  my ($b,@c);
  foreach $b ( @{$self->{blist}}, @{$self->{rlist}}, @{$self->{flist}},
    @{$self->{alist}}, @{$self->{llist}} )
    {
    @c = rotatePoint ($b->{x},$b->{y},$b->{z},$rx,$ry,$rz,$ax,$ay,$az);
    $b->{x} = $c[0]; $b->{y} = $c[1]; $b->{z} = $c[2];
    $b->{rx} += $ax; $b->{ry} += $ay; $b->{rz} += $az;
    clipAngle($b->{rx},$b->{ry},$b->{rz});
    }
  $self->{olist}->rotate($ax,$ay,$az,$rx,$ry,$rz);
  $self;
  }

sub rotateAll
  {
  # rotate all thingies around their center
  my $self = shift;
  my $ax = shift || 0; my $ay = shift || 0; my $az = shift || 0; # angle
  my ($b);
  foreach $b ( @{$self->{blist}}, @{$self->{rlist}}, @{$self->{flist}},
   @{$self->{alist}}, @{$self->{llist}} )
    {
    $b->{rx} += $ax; $b->{ry} += $ay; $b->{rz} += $az;
    clipAngle($b->{rx},$b->{ry},$b->{rz});
    }
  $self->{olist}->rotateAll ($ax,$ay,$az);
  $self;
  }

sub applyTextures
  {
  # extract from a given hash keys texture, sidetexture,
  # northtexture, southtexture, westtexture, easttexeture, ceilingtexture,
  # floortexture
  # and apply them to all brushes in bunch accordingly. This assumes unrotated
  # cubes
  # wedges and cylinders etc are special cases and are not working yet
  my $self = shift;
  my $p = shift || {};
  my $t = $p->{texture} || -1;
  # set east/west based on side
  my $es = $p->{sidetexture} || $t;
  my $ws = $p->{sidetexture} || $t;
  # but override with custom east/west
  $es = $p->{easttexture} if (defined $p->{easttexture});
  $ws = $p->{westtexture} if (defined $p->{westtexture});
  my $ns = $p->{northtexture} || $t;
  my $ss = $p->{southtexture} || $t;
  my $ft = $p->{floortexture} || $t;
  my $ct = $p->{ceilingtexture} || $t;
  # now apply, we assume that default the texture is set,
  # so return if nothing would change, anyway
  return $self if (($t == $es) && ($t == $ws) && ($t == $ns) && ($t == $ss)
    && ($t == $ft) && ($t == $ct));
  $t = { t => $t, s => 16, r => 0, u => 0, v => 0, } if (ref($t) ne 'HASH');
  $ss = { t => $ss, s => 16, r => 0, u => 0, v => 0, } if (ref($ss) ne 'HASH');
  $ws = { t => $ws, s => 16, r => 0, u => 0, v => 0, } if (ref($ws) ne 'HASH');
  $es = { t => $es, s => 16, r => 0, u => 0, v => 0, } if (ref($es) ne 'HASH');
  $ns = { t => $ns, s => 16, r => 0, u => 0, v => 0, } if (ref($ns) ne 'HASH');
  $ft = { t => $ft, s => 16, r => 0, u => 0, v => 0, } if (ref($ft) ne 'HASH');
  $ct = { t => $ct, s => 16, r => 0, u => 0, v => 0, } if (ref($ct) ne 'HASH');
  # fix missing values
  foreach my $h ($ft,$ct,$t,$ss,$ws,$es,$ns)
    {
    $h->{s} = 16 if (!defined $h->{s});
    $h->{r} = 0 if (!defined $h->{r});
    $h->{v} = 0 if (!defined $h->{v});
    $h->{u} = 0 if (!defined $h->{u});
    $h->{t} = -1 if (!defined $h->{t});
    $h->{a} = 0 if (!defined $h->{a});
    }
  foreach my $b (@{$self->{blist}})
    {
    next unless ($b->{kind} == cube());
    # use internal structures to avoid call
    $b->{faces}->[top()] = $ct if ($ct != $t);
    $b->{faces}->[bottom()] = $ft if ($ft != $t);
    $b->{faces}->[east()-1] = $es if ($es != $t);
    $b->{faces}->[west()-1] = $ws if ($ws != $t);
    $b->{faces}->[south()-1] = $ss if ($ss != $t);
    $b->{faces}->[north()-1] = $ns if ($ns != $t);
    }
  $self;
  }

sub scaleTexture
  {
  # scale each face if it has given texture num
  my $self = shift;
  my $brlist = $self->{blist};
  my ($texture,$newscale) = @_;
  my ($f,$b,$faces);
  foreach $b (@$brlist)
    {
    $faces = $b->{faces};
    foreach $f (@$faces)
      {
      $f->{s} = $newscale if ($f->{t} == $texture);
      }
    # jds; scale faces that inherit the matching default texture.
    # if the default texture for the brush matches the specified texture,
    # then also scale any faces that specify texture -1.
    if ($b->{texture} == $texture)
      {
      foreach $f (@$faces)
        {
        $f->{s} = $newscale if ($f->{t} == -1);
        }
      }
    }
  $self;
  }

sub changeTexture
  {
  # change textures that match a given texture number
  # it takes as parameters: either a ref to hash, where key is the number of texture
  # to replace, and the value either the new texture number or a texture hash, or a list
  # of key,value,key,value. Based on a idea by jds.
  # examples:
  #  changeTexture ( $hash );
  #  changeTexture ( { 1 => 2, 3 => 4 } );
  #  changeTexture ( 1,2,3,4 ); # same as before
  #  changeTexture ( 1 => { t => 1, r => 90 } ); # change every texture of 1 to rotation 90
  my $s = shift;
  my $p = $_[0];
  if (ref($p) ne 'HASH')
    {
    $p = { @_ };
    }
  my $brlist = $s->{blist}; my $changed = 0;
  foreach my $b (@$brlist)
    {
    foreach my $t (keys %$p)
      {
      if ($b->{texture} == $t)
        {
        if (ref($p->{$t}) eq 'HASH')
          {
          $b->{texture} = $p->{$t}->{t}; # deftext has only number!
          }
        else
          {
          $b->{texture} = $p->{$t}; # simple number
          }
        $changed++;
        }
      foreach my $f (@{$b->{faces}})
        {
        if ($f->{t} == $t)
          {
          if (ref($p->{$t}) eq 'HASH')
            {
            # copy over texture hash keys to change
            foreach my $k (keys %{$p->{$t}}) { $f->{$k} = $p->{$k}; }
            }
          else
            {
            $f->{t} = $p->{$t}; # simple number
            }
          $changed++;
          }
        }
      }
    }
  return $changed; # number of changed faces
  }


sub autoRoom
  {
  # Automatically add rooms to bunch.
  # It uses info {room} in each air brush or some clever rules.
  # an air()+room() always gets a room, an air()+noroom() never, and
  # for air() only it will be automatically choosen.
  # Rooms are, unlike in DromEd, generated with a growth of some
  # constant. This avoids some overlapping for large brushes due to the
  # factorial growth.
  # The eax settings are currently unsupported.
  # paramaters: keep (set to nonzero to keep existing rooms), base (object
  # id of base room, defaults to 1)
  # there is still the bug that a smaller air brsuh will sometimes get a room
  # inside a bigger room brush (automatic add only)
  my $self = shift;
  my $keep = shift || 0;
  my $base = shift || 1;
  my ($brlist,$b,$r,$rlist,$eax,$grav,$add);
  if ($keep == 0)
    {
    # throw away
    undef $self->{rlist}; $self->{rooms} = 0;
    }
  $brlist = $self->{blist};
  $eax = 0; $grav = 100;
  # add first forced rooms
  foreach $b (@$brlist)
    {
    next if ($b->{deleted});
    if (($b->{type} == air()) && ( ($b->{flags} & MROOM) != 0))
      {
      $self->addRoom ($b->{x},$b->{y},$b->{z},$b->{w}+$self->{radd},
                      $b->{l}+$self->{radd},$b->{h}+$self->{radd},
                      $eax,$grav,$b->{rx},$b->{ry},$b->{rz},$base);
      }
    }
  foreach $b (@$brlist)
    {
    next if ($b->{deleted});
    next if (($b->{flags} & MROOM) != 0); # forced are already done
    # only for air brushes
    if (($b->{type} == air()) && ( ($b->{flags} & MNOROOM) == 0))
      {
      $add = 1; # make do_add the default
      if ( ($b->{flags} & MROOM) == 0 ) # only for non-mrooms()
        {
        # check if breaks the rules if we would add it
        # (doesnt work correctly for rotated rooms!)
        $rlist = $self->{rlist};
        foreach $r (@$rlist)
          {
          # if first in second or vice-versa
          if ( ( ($b->{x} <= $r->{x}+$r->{l}/2) && ($b->{x} >= $r->{x}-$r->{l}/2) &&
                 ($b->{y} <= $r->{y}+$r->{w}/2) && ($b->{y} >= $r->{y}-$r->{w}/2) &&
                 ($b->{z} <= $r->{z}+$r->{h}/2) && ($b->{z} >= $r->{z}-$r->{h}/2) )
               ||
               ( ($r->{x} <= $b->{x}+$b->{l}/2) && ($r->{x} >= $b->{x}-$b->{l}/2) &&
                 ($r->{y} <= $b->{y}+$b->{w}/2) && ($r->{y} >= $b->{y}-$b->{w}/2) &&
                 ($r->{z} <= $b->{z}+$b->{h}/2) && ($r->{z} >= $b->{z}-$b->{h}/2) ) )
            {
            $add = 0; last; # dont add
            }
          }
        }
      $self->addRoom ($b->{x},$b->{y},$b->{z},$b->{w}+$self->{radd},
                      $b->{l}+$self->{radd},$b->{h}+$self->{radd},
                      $eax,$grav,$b->{rx},$b->{ry},$b->{rz},$base)
       if ($add == 1);
      }
    }
  # sometimes you have to build terrain by using solid blocks and carving out the
  # interior, like in dome()
  # this takes care of any solid with mroom
  foreach $b (@$brlist)
    {
    next if ($b->{deleted});
    # only for solid brushes
    if (($b->{type} == solid()) && ( ($b->{flags} & MROOM) != 0))
      {
      $self->addRoom ($b->{x},$b->{y},$b->{z},$b->{w}+$self->{radd},
                      $b->{l}+$self->{radd},$b->{h}+$self->{radd},
                      $eax,$grav,$b->{rx},$b->{ry},$b->{rz},$base);
      print "Room around solid brush $b->{time} at $b->{x} $b->{y} $b->{z}\n"
        if ($self->{debug});
      }
    }
  }

sub autoFlow
  {
  # Remove every flow brush and then automatically re-add flows to bunch.
  # It works only for fillwater/water brushes and honors MNOFLOW
  # Flows are, unlike in DromEd, generated with a growth of some constant.
  # This avoids some overlapping for large brushes due to the
  # factorial growth.
  my $self = shift;
  my ($brlist,$b);
  undef $self->{flist};
  $self->{flows} = 0;
  $brlist = $self->{blist};
  foreach $b (@$brlist)
    {
    # only for water brushes
    if ((($b->{type} == water()) || ($b->{type} == flood()))
     && ($b->{flags} != (MNOFLOW>>8)))
      {
      $self->addFlow ($b->{x},$b->{y},$b->{z},$b->{w}+$self->{radd},
                      $b->{l}+$self->{radd},$b->{h}+$self->{radd},
                      $b->{texture},$b->{rx},$b->{ry},$b->{rz});
      }
    }
  }

sub autoArea
  {
  # Add areas based on the different groups, areas are generated around all
  # brushes of a certain group. Not ready yet.
  my $self = shift;
  my ($brlist,@groups,$b);
  foreach $b (@$brlist)
    {
    }
  }

sub removeCube
  {
# delete any if its center is inside the boundaries
  my ($x,$y,$z,$w,$l,$h) = @_;
  my ($d,$b);
  my $self = shift; my $brlist = $self->{blist};
  $l = $l / 2; $h = $h / 2; $w = $w / 2;
  foreach $b (@$brlist)
    {
    if (($b->{x} < $x+$w) && ($b->{x} > $x-$w) &&
        ($b->{y} < $y+$l) && ($b->{y} > $y-$l) &&
        ($b->{z} < $z+$h) && ($b->{z} > $z-$h))
      {
      $b->{deleted} = 1;
      }
    }
  }

sub removeSphere
  {
# delete any with its center inside the boundaries
  my ($x,$y,$z,$r) = @_;
  $r = $r*$r;
  my $self = shift; my $brlist = $self->{blist};
  my $d;
  foreach $b (@$brlist)
    {
    $d = distance($b->{x},$b->{y},$b->{z},$x,$y,$z);
    $b->{deleted} = 1 if ($d < $r);
    }
  }

sub cloneRow
  {
  # clone yourself into a row of bunches
  # Warning: This can create lots of brushes/rooms/objects!
  my $self = shift;
  my ($count,$spacingx,$spacingy,$spacingz) = @_;
  $count = int($count);
  return $self if ($count < 2);
  my ($i,$c); my $b = Cowgen::Bunch->create("row_of_$self->{name}##num##");
  $b->debug( $self->{debug} );
  for ($i= 0; $i<$count; $i++)
    {
    $c = $self->clone();  $c->debug( $self->{debug} );
    $c->translate($spacingx*$i,$spacingy*$i,$spacingz*$i);
    $b->merge ( $c );
    }
  return $b; # row of self
  }

sub cloneArray
  {
  # clone yourself into an 3D-array of bunches
  # Warning: This can create lots of brushes/rooms/objects!
  my $self = shift;
  my ($countx,$county,$countz,$spacingx,$spacingy,$spacingz) = @_;
  $countx = int($countx); $county = int($county); $countz = int($countz);
  return $self if (($county < 2) || ($countx < 2) || ($countz < 2));
  my ($i,$j,$k,$c); my $b = Cowgen::Bunch->create("array_of_$self->{name}##num##");
  $b->debug( $self->{debug});
  for ($i= 0; $i<$countx; $i++)
    {
    for ($j= 0; $j<$county; $j++)
      {
      for ($k= 0; $k<$countz; $k++)
        {
        $c = $self->clone(); $c->debug($self->{debug});
        $c->translate($spacingx*$i,$spacingy*$j,$spacingz*$k);
        $b->merge ( $c );
        }
      }
    }
  return $b; # row of self
  }

sub cloneCircle
  {
  # clone yourself into a concentric circle array of bunches
  # starting with startradius, setting rings rings apart with spacingradius,
  # each using generating points of brushes.
  # Warning: This can create lots of brushes/rooms/objects!
  # not ready yet
  my $self = shift;
  my ($startradius,$spacingradius,$rings,$points) = @_;

  print "cloneCircle not implemented yet\n";
  $self;
  }

sub flip
  {
  my $self = shift;
  print "flip not implemented yet\n";
  }

sub fromText
  {
  # read one (some) bunch(s) from a textfile and merge them to meself
  # used as Cowgen::Bunch->create("name","textfile");
  # parameters: "file";
  my $self = shift;
  my $file = shift;

  $self->debug();
  my $levels;
  $levels->{1} = {bunch => "bunchlist"};
  $levels->{2} = {brush => "blist", room => "rlist", flow => "flist",
                  object => "olist", area => "alist", group => "glist",};
  $levels->{3} = {face => "faces" };
  # which names should be mapped/split to what
  # "room" is the old name for "flags"
  my $map = { size => "w,l,h", center => "x,y,z", rotation => "rx,ry,rz",
           deftexture => "texture", room => "flags"};

  my $top = Cowgen::Text::fromText ($file,$levels,$map); # oie fixed
  foreach my $key (@{$top->{bunchlist}})
    {
    $self->merge($key);
    }
  # set my name to first bunch
  $self->{name} = @{$top->{bunchlist}}[0]->{name}
    if (scalar @{$top->{bunchlist}} > 0);
  return;
  }

sub unrotateCubes
  {
  # Derotate cubes that are 90,180,270 degress rotated in one or more
  # of x,y,z axes and swap dimensions/faces so that the same endresult
  # is achieved. This makes certain calculations faster and optimization
  # easier. Currently takes min 0, max 18 steps per cube (avrg 13.5 if
  # rz,rx,ry are random distributed) per cube and is thus O(n).
  # Could be speed up if x,y,z derotations are combined into on so that
  # it will use only 6 copy actions per cube. Probably not worth the effort ;)
  # PII-300 takes about 18 seconds for 60.000 random cubes.
  my $self = shift;
  my $rotz = {
   90  => [ 1,2,3,0,4,5 ], 180 => [ 2,3,0,1,4,5 ], 270 => [ 3,0,1,2,4,5 ],
   };
  my $rotx = {
   90  => [ 0,5,2,4,1,3 ], 180 => [ 0,3,2,1,5,4 ], 270 => [ 0,4,2,5,3,1 ],
   };
  my $roty = {
   90  => [ 4,1,5,3,2,0 ], 180 => [ 2,1,0,3,5,4 ], 270 => [ 5,1,4,3,0,2 ],
   };
  my ($fl,$i,$u); my $unrot = 0; my $cubes = 0;
  foreach my $b (@{$self->{blist}})
    {
    next unless ($b->{kind} == cube());
    $u = 0; $cubes++;
    if (($b->{rz} == 90) || ($b->{rz} == 180) || ($b->{rz} == 270))
      {
      # swap width/length
      if ($b->{rz} != 180) { ($b->{w},$b->{l}) = ($b->{l},$b->{w}); }
      $fl = []; $i = 0;
      foreach (@{$rotz->{$b->{rz}}}) { $fl->[$_] = $b->{faces}->[$i++]; }
      # free old and store new, unrotate x
      $b->{faces} = $fl; $b->{rz} = 0; $u = 1;
      }
    if (($b->{rx} == 90) || ($b->{rx} == 180) || ($b->{rx} == 270))
      {
      # swap width/height
      if ($b->{rx} != 180) { ($b->{w},$b->{h}) = ($b->{h},$b->{w}); }
      $fl = []; $i = 0;
      foreach (@{$rotx->{$b->{rx}}}) { $fl->[$_] = $b->{faces}->[$i++]; }
      # free old and store new, unrotate x
      $b->{faces} = $fl; $b->{rx} = 0; $u = 1;
      }
    if (($b->{ry} == 90) || ($b->{ry} == 180) || ($b->{ry} == 270))
      {
      # swap length/height
      if ($b->{ry} != 180) { ($b->{l},$b->{h}) = ($b->{h},$b->{l}); }
      $fl = []; $i = 0;
      foreach (@{$roty->{$b->{ry}}}) { $fl->[$_] = $b->{faces}->[$i++]; }
      # free old and store new, unrotate x
      $b->{faces} = $fl; $b->{ry} = 0; $u = 1;
      }
    $unrot += $u;
    }
  print "Unrotated $unrot out of $cubes cubes (",
   scalar @{$self->{blist}}," brushes).\n" if ($self->{debug});
  }

sub combineCubes
  {
  # Combine cubes if they have same size, and a shared face and textures
  # are the same at the side faces.
  # Will take about n*n/2 (O(n*n)) if all cubes have the same type.
  # E.g. level with 500 solid cubes will take 150000 comparisations.
  # Level with 300 solid cubes and 200 air cubes will take 300*150+200*100
  # = 45000+20000 = 75000 comparisations.
  # LordBinclair has 2121 brushes, 1722 cubes (914 solid, 711 air, 46 water, 24 flood)
  # PII-300 takes about 18 seconds for 60.000 random cubes.
  my $self = shift;
  my ($fl); my $combined = 0; my $checked = 0;
  # fetch cubes from kind we have: O(n)
  my (%types,$togo,@todo,$i,$j,@left_over,$b1,$b2,@scratch);
  my $lasttime = 0;
  foreach my $b1 (@{$self->{blist}})
    {
    $types{$b1->{type}}++ if ($b1->{kind} == cube());
    # check if sorted by time
    # if yes, allows quicker out in checkInterset
    #if ($lasttime >= $b1->{time})
    #  {
    #  print "Arg! Brushes not sorted by time: $b1->{time} is lower then $lasttime last.\n";
    ##  return $self;
    #  }
    #$lasttime = $b1->{time};
    }
  # for each type with more than 2
  print "Optimizing: " if ($self->{debug});
  my $first = 0;
  foreach my $t (keys %types)
    {
    next if (($t != air()) && ($t != solid()));
    # run trough list and make list of all we have to check for combine O(n)
    print ", " if (($self->{debug}) && ($first != 0)); $first = 1;
    undef @todo;
    foreach my $b1 (@{$self->{blist}})
      {
      next if ($b1->{deleted});
      next if ($b1->{kind} != cube());
      next if ($b1->{type} != $t);
      push (@todo, $b1);
      }
    $togo = scalar @todo;
    print type2Name($t),"'s ($togo)" if ($self->{debug});
    # for each of them check against each other O(n*n)
    for ($i=0; $i<($togo-1); $i++)
      {
      $b1 = $todo[$i];
      next if ($b1->{deleted});
      for ($j=$i+1; $j<$togo; $j++)
        {
        $checked++;
        # in case of combine, leve earlier, since we have to recheck that cube anyway
        # against all other
        last if testCombine($b1,$todo[$j],\@left_over,\$combined,$self->{blist});
        }
      # cubes that got modifed need rechecking against all other of the same type
      #print "start leftover\n";
      while (@left_over > 0)
        {
        #print "Left_over: ",scalar @left_over,"\n";
        @scratch = @left_over; undef @left_over;
        foreach my $b1 (@scratch)
          {
          for ($j=0; $j<$togo; $j++)
            {
            next if ($todo[$j]->{deleted});
            # will compare b1 to b1, but wont burn us
            last if testCombine($b1,$todo[$j],\@left_over,\$combined,$self->{blist});
            }
          }
        }
      #print "done leftover\n";
      }
    }
  print "\n Removed $combined rendundand cubes by comparing $checked times.\n"
   if ($self->{debug});
  }

sub testCombine
  {
  # internal usage only. test and combines two cubes
  # return 0 for nothing done, 1 for combined
  my ($b1,$b2,$left_over,$combined,$blist) = @_;
  #print "\n check: ",
  # $b1->{time} => $b2->{time}  $b1->{w} $b1->{l} $b1->{h}  $b2->{w} $b2->{l} $b2->{h}";
  # case 1
  if ( ($b1->{w} == $b2->{w}) && ($b1->{h} == $b2->{h}) && ($b1->{y} == $b2->{y})
   && ($b1->{z} == $b2->{z}) )
    {
    if ( ($b1->{x} == $b2->{x} - $b2->{l}/2 - $b1->{l}/2) ||
         ($b1->{x} == $b2->{x} + $b2->{l}/2 + $b1->{l}/2) )
      {
      if (facesAreEqual($b1,$b2,1) && facesAreEqual($b1,$b2,3)
       && facesAreEqual($b1,$b2,4) && facesAreEqual($b1,$b2,5)
       && canCombine($blist,$b1,$b2))
        {
        # combine them by adjusting cube 1, remove cube2
        #print "\n1: $b1->{x},$b1->{y},$b1->{z}\n";
        #print "$b2->{x},$b2->{y},$b2->{z} $b2->{time} $b2->{l}\n";
        if ($b1->{x} == $b2->{x} - $b2->{l}/2 - $b1->{l}/2)
          {
          $b1->{x} += $b2->{l}/2;
          }
        else
          {
          $b1->{x} -= $b2->{l}/2;
          }
        $b1->{l} += $b2->{l}; $b2->{deleted} = 1;
        #print " comined case 1\n";
        push (@$left_over, $b1); $$combined++; return 1;
        }
      }
    }
  # case 2
  if ( ($b1->{l} == $b2->{l}) && ($b1->{h} == $b2->{h}) && ($b1->{x} == $b2->{x})
   && ($b1->{z} == $b2->{z}) )
    {
    if ( ($b1->{y} == $b2->{y} - $b2->{w}/2 - $b1->{w}/2) ||
         ($b1->{y} == $b2->{y} + $b2->{w}/2 + $b1->{w}/2) )
      {
      if (facesAreEqual($b1,$b2,0) && facesAreEqual($b1,$b2,2)
       && facesAreEqual($b1,$b2,4) && facesAreEqual($b1,$b2,5)
       && canCombine($blist,$b1,$b2))
        {
        # combine them by adjusting cube 1, remove cube2
        #print "\n2: $b1->{x},$b1->{y},$b1->{z}\n";
        #print "$b2->{x},$b2->{y},$b2->{z} $b2->{time} $b2->{w}\n";
        if ($b1->{y} == $b2->{y} - $b2->{w}/2 - $b1->{w}/2)
          {
          $b1->{y} += $b2->{w}/2;
          }
        else
          {
          $b1->{y} -= $b2->{w}/2;
          }
        $b1->{w} += $b2->{w}; $b2->{deleted} = 1;
        #print " comined case 2\n";
        push (@$left_over, $b1); $$combined++; return 1;
        }
      }
    }
  # case 3
  if ( ($b1->{w} == $b2->{w}) && ($b1->{l} == $b2->{l})  && ($b1->{x} == $b2->{x})
   && ($b1->{y} == $b2->{y}) )
    {
    if ( ($b1->{z} == $b2->{z} - $b2->{h}/2 - $b1->{h}/2) ||
         ($b1->{z} == $b2->{z} + $b2->{h}/2 + $b1->{h}/2) )
      {
      if (facesAreEqual($b1,$b2,0) && facesAreEqual($b1,$b2,1)
       && facesAreEqual($b1,$b2,2) && facesAreEqual($b1,$b2,3)
       && canCombine($blist,$b1,$b2))
        {
        # combine them by adjusting cube 1, remove cube2
        #print "\n3: $b1->{x},$b1->{y},$b1->{z}\n";
        #print "$b2->{x},$b2->{y},$b2->{z} $b2->{time} $b2->{h}\n";
        if ($b1->{z} == $b2->{z} - $b2->{h}/2 - $b1->{h}/2)
          {
         $b1->{z} += $b2->{h}/2;
          }
        else
          {
         $b1->{z} -= $b2->{h}/2;
          }
        $b1->{h} += $b2->{h}; $b2->{deleted} = 1;
        #print " comined case 3\n";
        push (@$left_over, $b1); $$combined++; return 1;
        }
      }
    }
  return;
  }

sub facesAreEqual
  {
  # internal only, compare to face hashes
  my ($f1,$f2,$n1,$n2) = @_;
  my $b1 = $f1; my $b2 = $f2;
  $n2 = $n1 if (!defined $n2);
  my $df1 = $f1->{texture};
  my $df2 = $f2->{texture};
  my ($e1,$e2);
  $f1 = $f1->{faces}->[$n1];  $f2 = $f2->{faces}->[$n2];
  foreach (keys %$f1)
    {
    $e1 = $f1->{$_}; $e2 = $f2->{$_};
    if ($_ eq 't')
      {
      $e1 = $df1 if ($f1->{t} == jorge());
      $e2 = $df2 if ($f2->{t} == jorge());
      }
    #if ($e1 ne $e2)
    #  {
    #  print "$b1 $b2 $f1->{$_},$f2->{$_} $e1 $e2\n";
    #  return 0;
    #  }
    return 0 if ($f1->{$_} ne $f2->{$_});
    }
  return 1;
  }

sub canCombine
  {
  # internal use only. checks wether we can really combine two brushes
  my ($blist,$b1,$b2) = @_;
  my ($mb,$mb2,@v);
  my ($left,$right,$top,$bottom,$front,$back);
  my ($i,$xl,$yl,$zl,$xr,$yr,$zr);
  foreach my $b (@$blist)
    {
    next if (($b->{time} <= $b1->{time}) || ($b->{time} >= $b2->{time}));
    #last if ($b->{time} >= $b2->{time}); # go out, since brushes are sorted by time
    next if ($b == $b1) || ($b == $b2);
    # is between, check if intersects with b2
    # (dumb check wether max sphere of b intersects with max sphere of b2
    $mb = max($b->{w},$b->{l},$b->{h});
    $mb2 = min($b2->{w},$b2->{l},$b2->{h});
    #print "$mb $mb2 $b->{time} between $b1->{time} and $b2->{time}\n";
    #print "$b2->{x},$b2->{y},$b2->{z},$b->{x},$b->{y},$b->{z}\n";
    if (distance($b2->{x},$b2->{y},$b2->{z},$b->{x},$b->{y},$b->{z})
      <= ($mb+$mb2)*($mb+$mb2))
      {
      # spheres intersect, make more torough check
      # if all vertexes of other brush are at either side (left/right, top/bottom
      # front/back of our brush, the cant possible intersect)
      @v = getVertices($b);
      # left == 1 => all points where left
      # if one of them survives, intersection is impossible (since convect)
      $left = 1; $right = 1; $top = 1; $bottom = 1; $front = 1; $back = 1;
      $xl = $b2->{x}-$b2->{l}/2; $xr = $b2->{x}+$b2->{l}/2;
      $yl = $b2->{y}-$b2->{w}/2; $yr = $b2->{y}+$b2->{w}/2;
      $zl = $b2->{z}-$b2->{h}/2; $zr = $b2->{z}+$b2->{h}/2;
      foreach ($i=0; $i < scalar @v; )
        {
        $back   = 0 if ($v[$i] >= $xl); $front = 0 if ($v[$i] <= $xr); $i++;
        $right  = 0 if ($v[$i] >= $yl); $left  = 0 if ($v[$i] <= $yr); $i++;
        $bottom = 0 if ($v[$i] >= $zl); $top   = 0 if ($v[$i] <= $zr); $i++;
        }
      if (($left | $right | $back | $front | $bottom | $top) != 0) # intersect
        {
        #print "$b1->{time} \& $b2->{time}: $b->{time}\n";
        #print "$left $right $bottom $top $front $back\n";
        return 0;
        }
      #print " but $b->{time} intersects\n";
      }
    }
  return 1;
  }

sub optimize
  {
  # unrotate cubes, then merge rendundant cubes together
  # see http://bloodgate.com/thief/cow/optimize.htm
  my $self = shift;
  $self->unrotateCubes(@_);
  $self->combineCubes(@_);
  return $self;
  }

sub sortBrushesByTime
  {
  # put all brushes in sorted order
  my $self = shift;
  @{$self->{blist}} = sort { $a->{time} <=> $b->{time} } @{$self->{blist}};
  }

sub alignTextures
  {
  # textures start at 0,0 and always have u,v 0..256 regardless of texture size
  # This means 32 (u,v) is +2 (or 16 per unit in DromEd
  # works only on unrotated cubes, so call optimize() beforehand!
  # parameters: tlist (ref to list of texture number to align, all other will be
  # left untouched; undefined list means align all), overwrite (=1: align
  # even if u and v of face are already set; default to no)
  my $self = shift;
  my $tlist = shift;
  my $ow = shift || 0; # default: no overwrite
  my $faces = 0; my $brushes = 0; my ($i,$u);
  my $looked = 0; my ($t,$dt);
  foreach my $b (@{$self->{blist}})
    {
    # only for unrotated cubes
    next if ($b->{deleted}); # dont bother with deleted stuff
    next if ($b->{kind} != cube());
    next if (($b->{rz} != 0) || ($b->{ry} != 0) || ($b->{rx} != 0));
    $i = -1; $u = 0; $dt = $b->{texture};
    foreach my $f (@{$b->{faces}})
      {
      $i++;
      $t = $f->{t}; $t = $dt if ($f->{t} == jorge());
      next if (defined $tlist) && (!defined $tlist->{$t});
      # dont align if already user aligned
      next if (($ow == 0) && (($f->{u} != 0) || ($f->{v} != 0)));
      #print "$b->{time} $b->{x},$b->{y},$b->{z} $b->{w},$b->{l},$b->{h} v: $f->{v} ";
      # for 0,1,2,3 the v is in z axis
      if ($i < 4)
        {
        $f->{v} = (($b->{z}+$b->{h}/2)*16) % 256;
        }
      #print "=> $f->{v}\n";
      if ($i == 0)
        {
        $f->{u} = (($b->{y}+$b->{w}/2)*16) % 256;
        }
      if ($i == 2)
        {
        $f->{u} = 256-(($b->{y}+$b->{w}/2)*16) % 256;
        }
      if ($i == 1)
        {
        $f->{u} = (($b->{x}+$b->{l}/2)*16) % 256;
        }
      if ($i == 3)
        {
        $f->{u} = 256-(($b->{x}+$b->{l}/2)*16) % 256;
        }
      # for 1 and 3, the v is in x axis (3=-v)
      $faces++; $u = 1;
      }
    $brushes += $u; $looked++;
    }
  print "Corrected texture alignment of $faces faces on $brushes brushes (looked at $looked).\n"
   if $self->{debug};
  return $self;
  }

sub toEnd
  {
  # move brush/room/object to end of time space
  # unlike dromed, this does increase the time space and creates a "hole" but it is
  # faster (no shuffling of tousands of brushes). Use compactTime() to compress time
  # space if you think you need it.
  # parameter: time (you get it when you create a brush/room/flow/object)
  my $s = shift;
  my $t = shift || return;
  return if (!defined $s->{ids}->{$t});
  my $entity = $s->{ids}->{$t};
  delete $s->{ids}->{$t};
  $t = $s->{time}++;
  $s->{ids}->{$t} = $entity;
  $entity->{time} = $t;
  return $t; # new time value
  }

sub toBegin
  {
  # move brush/room/object to begin of time space
  # unlike toEnd this shuffles time values around to get space, is therefore
  # much slower
  # unverified and not debugged! maybe buggy! especially if there are holes
  # on the way down from t to 1 it will fail!
  # parameter: time (you get it when you create a brush/room/flow/object)
  my $s = shift;
  my $t = shift || return;
  return if (!defined $s->{ids}->{$t});
  my $entity = $s->{ids}->{$t};
  my $ti;
  for ($ti = $t; $ti > 1; $ti--)
    {
    $s->{ids}->{$ti} = $s->{ids}->{$ti-1};
    $s->{ids}->{$ti}->{time} = $ti;
    }
  $s->{ids}->{1} = $entity; $entity->{time} = 1;
  return 1; # new time value
  }

sub compactTime
  {
  # compact the time values of all brushes so that they are linear
  # and counting from 1.  not ready yet.
  my $s = shift;
  return $s->{time}; # highest used
  }

sub iterate
  {
  # walk given list(s) and for each that match the criteria(s), call 'call'
  # return undef for error, otherwise $bunch. caller is called with following
  # parameter list: bunch (or olist for objects), ref_to_entity (brush/flow whatever), type
  # (entity type ("object", "brush", "light", "room" etc)), params
  # while you iterate over it, deleting things via $bunch->delete() is okay, but
  # adding something is not endorsed.
  # named parameters: over (list to walk; comma separated names blist; alist
  # rlist; flist; olist; llist; alternative names are brushes/areas/flows/rooms/objects/lights),
  # match (which property name should match what; hash of name=>value etc),
  # call (ref to sub to call), params (list of additional parameters to pass to caller)
  my $s = shift;
  my $p;
  if (ref($_[0]) ne 'HASH')
    {
    $p = { @_ };
    }
  else
    {
    $p = shift || return;
    }
  my $which = $p->{over} || return;
  my $match = $p->{match} || {}; # default match all
  my $call = $p->{call} || sub { shift; printHash (shift);} ; # default print
  my @p = split /,/,$which;
  my ($m,$e,$k);
  my $list = { objects => 'olist', flows => 'flist', areas => 'alist',
    brushes => 'blist', rooms => 'rlist', lights => 'llist',
    };
  my @params = @{$p->{params} || []};
  foreach my $l (@p)
    {
    $l = lc($l); # need it later lowercase to pass to call
    $k = $list->{$l} || next;
    $s->{olist}->iterate($p) if ($k eq 'olist');
    next unless $k =~ /^[bfarl]list$/; # only lists
    ENTITY:
    foreach $e (@{$s->{$k}})
      {
      foreach $m (keys %$match)
        {
        next ENTITY if ($e->{$m} ne $match->{$m});
        }
      &{$call} ($s,$e,$l,@params);
      }
    }
  return $s;
  }

1;

