Kategorien
Computer Programmierung

Java-Stream-API

Weil ich gestern das Script hatte: die Stream-API von Java ist schon geil für einige Anwendungsfälle. Kurz, prägnant und lesbar.

Ausschnitt vom Code gestern: einmal Pfad bis in die Unterverzeichnisse traversieren und die Dateien bearbeiten:

try (Stream<Path> paths = Files.walk(pathSource)) {
  paths
    .filter(Files::isRegularFile)
    .filter(path -> path.toString().endsWith(EXTENSION))
    .forEach(path -> processFile(path, args[1], Integer.parseInt(args[2])));
}

Gerade das Filter-Map-Reduce-Pattern macht was her (Map ist im Beispiel nicht dabei).

Aber ich finde es nicht einfach zu lernen und da ist viel try-error und Nachlesen (ich hab mir ein Buch gekauft!) dabei, gerade bei Syntax, Variablen-Zugriff und sonstigen Nebeneffekten.

Die Funktion processFile z.B. darf keine Exception werfen, sondern nur RuntimeException, weil das sonst nicht compiliert. Hat bestimmt seinen Grund, ist aber programmtechnisch blöd, weil nicht mehr so sauber.

Aber irgendwas ist immer…

Kategorien
Computer Programmierung

Java als Scriptsprache

Mal etwas anderes als Filme: Seit Java 11 kann man Java als Scriptsprache einsetzen, das heißt, direkt eine Java-Datei aufrufen.

Im Endeffekt schreibt man eine Klasse mit main-Methode, die die Arbeit erledigt (man kann wohl mehrere Klassen in eine Datei packen, aber wer macht sowas).

Was rauskommt, ist typisches Scripting der ersten Stunde: Spaghetticode, der eine Aufgabe erledigt und nicht wiederverwendet werden kann. Wenn man sich dessen bewusst ist, ist das tatsächlich hilfreich.

Vorteil: das Script ist plattformübergreifend nutzbar
Nachteil: Java ist nicht für Scripting entworfen, der Code ist grauenhaft, es ist nur eine Datei erlaubt, also keine Wiederverwendung

Ich habe das jetzt mal ausprobiert, da ich schnell eine Lösung brauchte und mich in Python etc. immer erst einarbeiten muss und auf meinen Rechnern überall Java installiert ist.

Ob ich das dauerhaft Bash-Scripts (die echt nicht schön aussehen) und Ant-Scripts vorziehe – mal so, mal so, vermute ich. Alles mit Aufruf von Programmen auf dem Rechner wird wohl bash oder Ant bleiben, bei dem Rest muss ich gucken.

Mein aktuelles Beispiel: neuen Dateinamen für eine Datei finden mit Informationen, die aus dem XML (kml) geholt werden. Dafür sieht es ganz ok aus…

Aufruf: java RenameKML.java <path> <color> <width>

import java.awt.Color;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.Writer;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;

public class RenameKML {

  private final static String EXTENSION = ".kml";

  /**
   * Main method.
   */
  public static void main(String[] args) {

    System.out.println("Rename KML");

    try {

      if (args.length < 3) {
        throw new Exception("Missing parameters.");
      }

      Path pathSource = Path.of(args[0]);
      if (Files.exists(pathSource)) {
        try (Stream<Path> paths = Files.walk(pathSource)) {
          paths
              .filter(Files::isRegularFile)
              .filter(path -> path.toString().endsWith(EXTENSION))
              .forEach(path -> processFile(path, args[1], Integer.parseInt(args[2])));
        }
      } else {
        throw new FileNotFoundException(pathSource.toString());
      }

    } catch (Exception e) {

      System.err.println();
      e.printStackTrace();
      System.err.println();
      printUsage();

      System.exit(1);

    }

  }

  /**
   * Usage information.
   */
  private static void printUsage() {

    System.out.println("Usage:\tjava RenameKML.java <path> <color> <width>");
    System.out.println("\tcolor: RRGGBBAA");
    System.out.println("");

  }

  /**
   * Process single file.
   *
   * A normal Exception cannot be declared, because this method is called from
   * withni Stream.forEach, only RuntimeException is allowed there.
   */
  private static void processFile(
      final Path theXMLFile,
      final String theColor,
      final int theWidth
  ) {

    System.out.println(String.format("Processing %s with color %s and width %d", theXMLFile.toString(), theColor, theWidth));

    try {

      String sXMLContent = Files.readString(theXMLFile);
      sXMLContent = cleanKML(sXMLContent, theColor, theWidth);


      Path pathOutFile = Path.of(theXMLFile.getParent().toAbsolutePath().toString(), String.format("%s%s", getNewFilename(sXMLContent), EXTENSION));

      writeKMLFile(pathOutFile, sXMLContent);

      if (!theXMLFile.toAbsolutePath().equals(pathOutFile.toAbsolutePath())) {
        System.out.println(String.format("\tDeleting: %s", theXMLFile.toString()));
        Files.delete(theXMLFile);
      }

    } catch (Exception e) {
      throw new RuntimeException(e);
    }

  }

  /**
   * Clean KML from unused or strange tags/content.
   */
  private static String cleanKML(
      final String theKML,
      final String theColor,
      final int theWidth
  ) throws Exception {

    String sKMLColor = theColor.replaceAll("(..)(..)(..)(..)", "$4$3$2$1");

    String sKMLCleaned = theKML;
    // IconStyles
    sKMLCleaned = sKMLCleaned.replaceAll(
      "<Style id=\".*\"><IconStyle>.*?</IconStyle></Style>",
      ""
    );
    // Folder
    sKMLCleaned = sKMLCleaned.replaceAll(
      "<Folder>(.*)</Folder>",
      "$1"
    );
    // Recording Placemarks
    sKMLCleaned = sKMLCleaned.replaceAll(
      "<Placemark>.*?Recording.*?</Placemark>",
      ""
    );
    // Fill name, remove other
    sKMLCleaned = sKMLCleaned.replaceAll(
      "<name>.*<description>(.*)</description>(<Placemark>)",
      "$2<name>$1</name>"
    );
    // Remove double names
    sKMLCleaned = sKMLCleaned.replaceAll(
      "<name>(.*)</name><name>.*?</name>",
      "<name>$1</name>"
    );
    // Remove last name/description
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(</Placemark>)<name>.*</description>",
      "$1"
    );
    // Remove strange underlines
    sKMLCleaned = sKMLCleaned.replaceAll(
      "<u>(.*?)</u>",
      "$1"
    );

    // reformatting description

    // time conversion (strangely complicated)
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(\\d{2}\\:\\d{2}\\:\\d{2}) vorm\\. ",
      "$1 "
    );
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(\\d{1}\\:\\d{2}\\:\\d{2}) vorm\\. ",
      "0$1 "
    );
    Matcher matchTime = Pattern.compile("(\\d{1,2})(\\:\\d{2}\\:\\d{2}) nachm\\. ").matcher(sKMLCleaned);
    while (matchTime.find()) {
      sKMLCleaned = sKMLCleaned.replaceAll(
        String.format("%s%s nachm\\. ", matchTime.group(1), matchTime.group(2)),
        String.format("%d%s ", Integer.parseInt(matchTime.group(1)) + 12, matchTime.group(2))
      );
    }

    // Duration
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(Duration\\: )(\\d{1}h\\d{2}'\\d{2}\")",
      "$10$2"
    );
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(Duration\\: )(\\d{2})h(\\d{2})'(\\d{2})\"",
      "$1$2:$3:$4"
    );
    // Description itself
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(<description><!\\[CDATA\\[)Start\\: (\\d{2}\\.\\d{2}\\.\\d{4}).*?(\\d{2}\\:\\d{2})\\:\\d{2} .*?End\\: \\d{2}\\.\\d{2}\\.\\d{4}.*?(\\d{2}\\:\\d{2})\\:\\d{2} .*?Duration\\: (\\d{2}\\:\\d{2})\\:\\d{2}.*?Length\\: (.*?)km.*?Avg Speed\\: (.*?)km h.*?Max Speed\\: (.*?)km h.*?Low\\: (.*?)m.*?High\\: (.*?)m.*?Gain\\: (.*?)m.*(\\]\\]></description>)",
      "$1Am $2 von $3 bis $4 Uhr, Dauer: $5 h, Strecke: $6 km, Ø: $7 km/h, v_max: $8 km/h, Höhenmeter: $11 m von $9 m bis $10 m$12"
    );

    // /reformatting description

    // Single TimeStamp
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(</styleUrl>)(<gx:Track>)(<altitudeMode>.*</altitudeMode>)(<when>.*?</when>)",
      "$1<TimeStamp>$4</TimeStamp>$2$3"
    );
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(<altitudeMode>.*</altitudeMode>)<when>.*</when>",
      "$1"
    );

    // Change color
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(<color>).{8}(</color>)",
      String.format("$1%s$2", sKMLColor)
    );

    // Change width
    sKMLCleaned = sKMLCleaned.replaceAll(
      "(<width>).(</width>)",
      String.format("$1%d$2", theWidth)
    );

    return sKMLCleaned;

  }

  /**
   * Write KML file, tidy up first.
   */
  private static void writeKMLFile(
      final Path theOutFile,
      final String theXMLContent
  ) throws Exception {

    System.out.println(String.format("\tWriting: %s", theOutFile.toString()));
    Files.writeString(theOutFile, tidyXML(theXMLContent), StandardOpenOption.CREATE, StandardOpenOption.WRITE);

  }

  /**
   * Tidy up XML.
   */
  private static String tidyXML(
      final String theXMLContent
  ) throws Exception {

    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteArrayInputStream(theXMLContent.getBytes()));
    doc.getDocumentElement().normalize();

    Transformer transformer = TransformerFactory.newInstance().newTransformer();
    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    DOMSource source = new DOMSource(doc);

    String sResult = null;

    try (Writer swXML = new StringWriter()) {

      transformer.transform(source, new StreamResult(swXML));
      sResult = swXML.toString();

    }

    return sResult;

  }

  /**
   * Get new filename from name and date within KML file.
   */
  private static String getNewFilename(final String theXML) throws Exception {

    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteArrayInputStream(theXML.getBytes()));
    doc.getDocumentElement().normalize();


    NodeList nList = doc.getElementsByTagName("name");

    if (nList.getLength() != 1) {
      throw new Exception("more than one or no name tag");
    }

    String sName = nList.item(0).getTextContent();


    nList = doc.getElementsByTagName("when");

    if (nList.getLength() != 1) {
      throw new Exception("more than one or no when tag");
    }

    LocalDateTime dteWhen = LocalDateTime.parse(nList.item(0).getTextContent(), DateTimeFormatter.ISO_OFFSET_DATE_TIME);

    return String.format("%s_%s",
      dteWhen.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
      cleanFilename(sName, true)
    );

  }

  /**
   * Clean filename from special characters.
   */
  private static String cleanFilename(
      final String theFilename,
      final boolean convertUmlauts
  ) {

    String sConverted = theFilename;

    sConverted = sConverted
        .replace(" ", "_")
        .replace("?", "_")
        .replace(":", "_")
        .replace(";", "_")
        .replace(",", "_")
        .replace("/", "_")
        .replace("\\", "_")
        .replace("*", "_")
        .replace("(", "_")
        .replace(")", "_")
        .replace("[", "_")
        .replace("]", "_")
        .replace("{", "_")
        .replace("}", "_")
        ;

    if (convertUmlauts) {
      sConverted = sConverted
          .replace("ä", "ae")
          .replace("Ä", "Ae")
          .replace("ö", "oe")
          .replace("Ö", "Oe")
          .replace("ü", "ue")
          .replace("Ü", "Ue")
          .replace("ß", "ss")
          ;
    }

    return sConverted;

  }

}

/* EOF */

Als Vergleich hier das Ant-Script, dass die XML-Umwandlung vornimmt, könnt Ihr ja selbst sehen, was Ihr lesbarer findet…

<?xml version="1.0" encoding="UTF-8" ?>

<project name="kml" default="help" basedir=".">

	<import file="/home/ekleinod/working/git/edgeutils/ant/ant-commons.xml"/>

	<target name="kml" depends="clearLog" description="Clean all KML files in directory.">

		<fail message="Pfad für die KML-Dateien nicht gesetzt (Farbe: RRGGBBTT): ant kml -Dkml:path=&quot;.&quot; -Dkml:color=&quot;14C8F0CE&quot; -Dkml:width=&quot;5&quot;" unless="kml:path" />
		<fail message="Farbe für die KML-Dateien nicht gesetzt (Farbe: RRGGBBTT): ant kml -Dkml:path=&quot;.&quot; -Dkml:color=&quot;14C8F0CE&quot; -Dkml:width=&quot;5&quot;" unless="kml:color" />
		<fail message="Breite für die KML-Dateien nicht gesetzt (Farbe: RRGGBBTT): ant kml -Dkml:path=&quot;.&quot; -Dkml:color=&quot;14C8F0CE&quot; -Dkml:width=&quot;5&quot;" unless="kml:width" />

		<echo message="Bearbeite KML-Dateien in '${kml:path}', Farbe '${kml:color}', Breite '${kml:width}'" />

		<echo message="Remove IconStyles" />
		<replaceregexp
				encoding="utf-8"
				match="&lt;Style id=&quot;.*&quot;&gt;&lt;IconStyle&gt;.*&lt;/IconStyle&gt;&lt;/Style&gt;"
				replace=""
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Remove Folder" />
		<replaceregexp
				encoding="utf-8"
				match="&lt;Folder&gt;(.*)&lt;/Folder&gt;"
				replace="\1"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Remove Recording Placemarks" />
		<replaceregexp
				encoding="utf-8"
				match="&lt;Placemark&gt;&lt;TimeStamp&gt;.*&lt;/TimeStamp&gt;&lt;/Placemark&gt;"
				replace=""
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Fill name, remove other" />
		<replaceregexp
				encoding="utf-8"
				match="&lt;name&gt;.*&lt;description&gt;(.*)&lt;/description&gt;(&lt;Placemark&gt;)"
				replace="\2&lt;name&gt;\1&lt;/name&gt;"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Remove last name/description" />
		<replaceregexp
				encoding="utf-8"
				match="(&lt;/Placemark&gt;)&lt;name&gt;.*&lt;/description&gt;"
				replace="\1"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Remove double names" />
		<replaceregexp
				encoding="utf-8"
				match="&lt;name&gt;(.*)&lt;/name&gt;&lt;name&gt;.*&lt;/name&gt;"
				replace="&lt;name&gt;\1&lt;/name&gt;"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Remove strange underlines" />
		<replaceregexp
				encoding="utf-8"
				match="&lt;u&gt;(.*)&lt;/u&gt;"
				replace="\1"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<antcall target="timeconversion" />

		<echo message="Reformat description" />
		<replaceregexp
				encoding="utf-8"
				match="(Duration\: )(\d{1}h\d{2}'\d{2}&quot;)"
				replace="\10\2"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="(Duration\: )(\d{2})h(\d{2})'(\d{2})&quot;"
				replace="\1\2:\3:\4"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="(&lt;description&gt;&lt;!\[CDATA\[)Start\: (\d{2}\.\d{2}\.\d{4}).*?(\d{2}\:\d{2})\:\d{2} .*?End\: \d{2}\.\d{2}\.\d{4}.*?(\d{2}\:\d{2})\:\d{2} .*?Duration\: (\d{2}\:\d{2})\:\d{2}.*?Length\: (.*?)km.*?Avg Speed\: (.*?)km h.*?Max Speed\: (.*?)km h.*?Low\: (.*?)m.*?High\: (.*?)m.*?Gain\: (.*?)m.*(\]\]&gt;&lt;/description&gt;)"
				replace="\1Am \2 von \3 bis \4 Uhr, Dauer: \5 h, Strecke: \6 km, Ø: \7 km/h, v_max: \8 km/h, Höhenmeter: \11 m von \9 m bis \10 m\12"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Insert TimeStamp" />
		<replaceregexp
				encoding="utf-8"
				match="(&lt;/styleUrl&gt;)(&lt;gx:Track&gt;)(&lt;altitudeMode&gt;.*&lt;/altitudeMode&gt;)(&lt;when&gt;.*?&lt;/when&gt;)"
				replace="\1&lt;TimeStamp&gt;\4&lt;/TimeStamp&gt;\2\3"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Remove other TimeStamps" />
		<replaceregexp
				encoding="utf-8"
				match="(&lt;altitudeMode&gt;.*&lt;/altitudeMode&gt;)&lt;when&gt;.*&lt;/when&gt;"
				replace="\1"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Fill color (RGBT)" />
		<replaceregexp
				encoding="utf-8"
				match="(&lt;color&gt;).{8}(&lt;/color&gt;)"
				replace="\1${kml:color}\2"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<echo message="Reformat color (RGBT -> TBGR)" />
		<replaceregexp
				encoding="utf-8"
				match="(&lt;color&gt;)(..)(..)(..)(..)(&lt;/color&gt;)"
				replace="\1\5\4\3\2\6"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Fill width" />
		<replaceregexp
				encoding="utf-8"
				match="(&lt;width&gt;).(&lt;/width&gt;)"
				replace="\1${kml:width}\2"
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

		<echo message="Tidy XML" />
		<apply executable="xmllint" parallel="false" force="true" dir="${kml:path}" relative="true">
			<arg value="--format" />
			<arg value="--output" />
			<targetfile />
			<srcfile />
			<fileset dir="${kml:path}" includes="*.kml" />
			<mapper type="identity" />
		</apply>

	</target>

	<target name="timeconversion" depends="clearLog">

		<echo message="Reformat time" />

		<replaceregexp
				encoding="utf-8"
				match="(\d{1}\:\d{2}\:\d{2}) vorm\. "
				replace="0\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="(\d{2}\:\d{2}\:\d{2}) vorm\. "
				replace="\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="1(\:\d{2}\:\d{2}) nachm\. "
				replace="13\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="2(\:\d{2}\:\d{2}) nachm\. "
				replace="14\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="3(\:\d{2}\:\d{2}) nachm\. "
				replace="15\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="4(\:\d{2}\:\d{2}) nachm\. "
				replace="16\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="5(\:\d{2}\:\d{2}) nachm\. "
				replace="17\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="6(\:\d{2}\:\d{2}) nachm\. "
				replace="18\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="7(\:\d{2}\:\d{2}) nachm\. "
				replace="19\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="8(\:\d{2}\:\d{2}) nachm\. "
				replace="20\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="9(\:\d{2}\:\d{2}) nachm\. "
				replace="21\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="10(\:\d{2}\:\d{2}) nachm\. "
				replace="22\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="11(\:\d{2}\:\d{2}) nachm\. "
				replace="23\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>
		<replaceregexp
				encoding="utf-8"
				match="12(\:\d{2}\:\d{2}) nachm\. "
				replace="00\1 "
				flags="g">

			<fileset dir="${kml:path}" includes="*.kml" />

		</replaceregexp>

	</target>

</project>

<!-- EOF -->
Kategorien
Computerspiele Kritik

Spielkritik: Tomb Raider: Underworld (2008)

Tomb Raider: Underworld, der neunte Teil der Reihe und der letzte „richtige“, „altmodische“, „originale“ Teil der Serie, wie auch immer man das nennen will.

Kurz erklärt: bis hierher war das Spielprinzip: Lara geht durch eine Welt, die sie erkundet, Fallen ausweicht, Rätsel löst, ab und an jemanden umbringt und insgesamt ein großes Rätsel um ein Artefakt löst und so die Welt rettet.

Dieser Teil ist der letzte, der das in dieser Art tut, das Reboot von 2013 geht da andere Wege, hier kämpfen wir uns mit Lara durch eine Originstory und nur am Rande lösen wir sehr simple Rätsel. Das ist jetzt nicht verbittert gemeint, neun Spiele lang dasselbe Prinzip: wahrscheinlich war es gut, hier mal kräftig aufzuräumen, ob man allerdings den Kern der Spiele so verändern musste – kann man so oder so sehen. Das Reboot gefällt mir, ist halt nur ein anderes Spiel. Wie dem auch sei:

Auf in den letzten Kampf auf der PS 3 mit der neuen Underworld-Engine, die auch heute noch die Spiele nutzen.

Wir spielen die übliche Levelstruktur, zusammengehalten durch eine dünne Geschichte, wir besuchen das Mittelmeer, Thailand, Mexiko, die Arktis und Croft Manor.

An sich ist das Spiel nicht schlecht. Es sieht fantastisch aus, Lara wurde wieder verbessert, die Bewegungen sind klasse, an sich stimmt alles. Bis auf das Leveldesign und den Schwierigkeitsgrad.

Es gibt Gegenden, da schlunzt man durch und es gibt Gegenden, da ist es unglaublich zäh und man muss eine Sequenz auf die Sekunde genau durchführen, um weiterzukommen. Meist trifft das Boni, so dass der Spielfluss bewahrt bleibt, aber es ist nervig, wenn man nach einer Stunde immer noch an einer 10-Sekunden-Sequenz hängt. Das Ganze mit langen Ladezeiten zwischen den Versuchen.

Die Rätsel sind sehr, sehr einfach konzipiert, da ist kein Nachdenken, sondern nur Ausführen nötig. Schade.

Dazu kommt, dass Croft Manor explodiert. WTF? Warum?

Außerdem, und das ist nicht gut, konnte ich das Spiel nicht beenden, weil es eine Sequenz gab, die ich nicht geschafft habe, in einem Turm hochzuspringen auf versenkbaren Plattformen, dabei ein Monster abschießen und auf Flammen achten. Das Ganze vielleicht eine halbe Minute pixelgenau ohne Zwischenspeichern oder Umsehen. Pixelgenau meint pixelgenau. Lange Ladezeiten zwischen den Versuchen.

Ich hab es versucht. Zwei Stunden lang. Dann hab ich aufgegeben und das Spiel nicht mehr angefasst und ich werde es auch nicht mehr anfassen. Immerhin ist der Controller heilgeblieben – die halten halt viel aus.

Bis dahin wurde ich angenehm unterhalten mit eingestreuten Frustmomenten, diese Sequenz hat das zunichte gemacht.

Bis dahin hatte ich nur ein Spiel nicht beenden können: DOOM 2 und das ist für Normalsterbliche auch nicht machbar aber nicht schlimm, weil DOOM.

Hier setzte die Sequenz einfach den Ton: ein ordentliches Spiel durch die Spielmechanik unspielbar gemacht. Sehr, sehr schade.

Dabei hatte insbesondere Uncharted parallel gezeigt, wie ein Tomb-Raider aussehen kann.

Das Spiel kam auch insgesamt nicht so gut an, so dass erst 2013 ein Nachfolger kam, das angesprochene Reboot ohne Altlasten.

Fazit: leider mit dem Arsch die gute Atmosphäre eingerissen.

Kategorien
Computerspiele Kritik

Spielkritik: Tomb Raider: Anniversary (2007)

Ich hab den siebenten Teil ausgelassen (Legend), dafür war der sechste doch zu schwach und stieg mit Tomb Raider: Anniversary (2007) auf der PS2 wieder ein. Das schien mir eine sichere Sache zu sein – der Teil ist ein Remake des ersten Teils mit der Legend-Engine von Crystal Dynamics, die schon beim Vorgänger eingesetzt wurde.

Ich wurde nicht enttäuscht, das Spiel ist eine gelungene Neuauflage des ersten Teils, spieltechnisch nicht neu, sah aber sehr schön aus. Ich kannte mich ja auch schon aus und fühlte mich auch gleich wohl. Laras Aussehen wurde wieder verbessert, die Bewegungen modernisiert und die Spielfreude beibehalten. Die Level sind gleich geblieben, hier gibt es keine Veränderungen.

Viel mehr gibt es nicht zu sagen, das ist einfach ein überzeugendes Spiel.

Fazit: sehr gelungene Neuauflage des ersten Spiels.

Kategorien
Computerspiele Kritik

Spielkritik: Tomb Raider: The Angel of Darkness (2003)

Tomb Raider: The Angel of Darkness – der sechste Teil der Serie, veränderte Rahmenbedingungen erforderten Veränderungen am Spiel.

Zunächst einmal wurden die Rechner immer besser und auch die Spielekonsolen gingen in die nächste Generation, allen voran die Playstation 2. Das hieß, die Engine musste überarbeitet und wahrscheinlich komplett neu geschrieben werden, insbesondere die Playstation erforderte komplett neue Programmierung, die dazu noch inkompatibel mit dem Rest der Welt war. Also: eigene Engine oder einkaufen, die Entscheidung fiel für eigene Engine.

Dazu kam ein großer Projektdruck, die Cashcow war schon zwei Jahre nicht mehr erschienen, das Publikum lechzte nach mehr und bekam auch abseits von Tomb Raider gutes Material geliefert.

Das führte leider dazu, dass ein unfertiges Spiel auf den Markt geworfen wurde, ob das Schuld der Entwickler, von Core oder Eidos war, kann ich nicht beurteilen, dafür das fertige Produkt.

Ich hab es für den PC gekauft und das Spiel sah wirklich gut aus. Die Bewegungen von Lara wurden ebenfalls angepasst, auch hier deutliche Verbesserungen.

Aber.

Die Steuerung war komplett kaputt. Lara tat nicht, was sie sollte. Im einem Tomb-Raider-Spiel! Präzision war überhaupt nicht vorhanden und das Spiel war so buggy wie ich keins vor- oder nachher in der Hand hatte. Ich konnte bis zu einer Stelle spielen, das war mit der Steuerung schon schwierig genug, dann blieb Lara an einer Stelle stehen und durch einen Bug kam sie da nicht mehr raus. Egal, was man tat. Reproduzierbar.

2003 war das Internet dafür gut, zu sehen, dass es anderen genauso ging, einen schnellen Patch gab es aber nicht.

Also hab ich das Spiel weggelegt und abgeschrieben, ich kann also nicht sagen, ob es noch eine gute, bugfreie Variante mit guter Steuerung gibt.

Für einen so großen Titel war das einfach sehr, sehr schwach und enttäuschend. Das sahen viele so, auch Eidos, die daraufhin Core die Serie abnahmen und an Crystal Dynamics gaben.

Fazit: ganz schlecht, wegen Bugs nicht spielbar.

Kategorien
Computerspiele Kritik

Spielkritik: Tomb Raider: Die Chronik (2000)

Tomb Raider: Die Chronik (Tomb Raider V) – Lara ist tot, was liegt näher, als dass sich drei Freunde bei einer Trauerfeier Geschichten über Lara erzählen – jede dieser Geschichten ist ein Level.

Core Design verbessert zum letzten Mal die alte Spieleengine und befreit sich davon, eine zusammenhängende Geschichte erzählen zu müssen. Damit können die Level unterschiedlich ausfalen ohne dass das durch eine Geschichte mühsam zusammengehalten werden muss.

Das gelingt auch sehr gut, wir suchen in Rom den Stein der Weisen, in einem russischen U-Boot den Speer des Schicksals, in Irland als Mädchen Dämonen (wieder als Mädchen – ach nö) und in New York City das Iris-Artefakt. Zu guter Letzt suchen wir noch die sterblichen Überreste Laras in den Ruinen, finden aber nur ihren Rucksack.

Das ist nicht sonderlich innovativ, aber das macht mir ja nichts aus…

Lara kann jetzt aus Nischen springen, auf Seilen balancieren und hat andere coole Moves drauf. Viel mehr Neues ist nicht.

Die Level sind ordentlich, die Story vernachlässigbar, aber insgesamt ist es ein deutlicher Qualitätsabfall gegenüber den Vorgängerspielen. Das Spiel ist immer noch gut aber auch nicht mehr. Es fehlt das gewisse Etwas, an das man sich erinnert: übergroße Pyramiden, schwierige Rätsel, übermächtige Gegner. Man merkte deutlich, dass die Entwickler eine Pause brauchten.

Die ihnen Eidos als Mutterfirma von Core nicht gab. Stattdessen wurde der Druck aufgebaut, ein Spiel nach dem anderen produzieren und nach vier Knallern musste irgendwann auch mal Schluss sein. Das hat Eidos verhindert und deutlich gezeigt, wie Geldgier und Druck nicht zu guten Ergebnissen führen. Das Spiel war auch nicht der Verkaufsschlager, so dass nach diesem Spiel eine Zwangspause eintrat.

Leider wurde diese Pause nicht dafür genutzt, den Entwickler:innen Raum und Zeit für neue Visionen und solide Arbeit zu geben, Eidos erwies sich hier als der Archetyp der nichtkreativen Schinderei, die keine guten Ergebnisse hervorbringt, dennoch niemenden zum Umdenken bringt. Kreativität ist halt nur zu einem gewissen Teil planbar und genau das muss bei Geschäftsplänen auch berücksichtigt werden.

Wird es aber nicht, wie viele Qualitätseinbussen bei längerer Laufzeit zeigen, die Gründe mögen unterschiedlich sein.

Erfreuen wir uns am gelungenen, wenngleich etwas einfallslosen fünten Teil, der sechste (Spoiler) wird richtig mies…

Fazit: ordentliches Spiel ohne Höhepunkte.

Kategorien
Computerspiele Kritik

Spielkritik: Tomb Raider IV: The Last Revelation (1999)

Tomb Raider IV: The Last Revelation – wieder Core Design, wieder die gleiche, leicht verbesserte Engine – wieder das gleiche Spiel mit neuen Orten und Figuren.

Wir müssen den Fluch des Amuletts von Horus brechen, der Seth aufgeweckt hat (kurz zusammengefasst).

Wir starten als junge Lara, warum auch immer. Meist ein Zeichen dafür, dass den Geschichtenschreibern nicht mehr viel einfällt und die Herkunft der Heldin ergründet werden muss, zum Glück ist das nur der Einstiegslevel, dann geht es wie gewohnt weiter, diesmal von Kambodscha nach Ägypten, wo mehrere Orte besucht werden: Tal der Könige, Karnak, Alexandria, Kairo, Gizeh.

Die Pistolen sind wieder da.

Es wurden viele Elemente verändert, zum Guten und zum Schlechten. Dem Zeitgeist folgend wurden die Waffen komplizierter, es gab mehr Munition zur Auswahl mit unterschiedlicher Wirkung. Warum auch immer – hätte es nicht gebraucht.

Lara kann jetzt Stangen hochklettern, um die Ecke greifen und Türen eintreten – immerhin.

Leider wurde auch der Todfeind und Kardinalfehler von Computerspielen eingeführt: unsterbliche Gegner. Was das soll – keine Ahnung. Die stören zwar nicht direkt und sind handhabbar, aber es ist keine schöne Sache.

Und ein weiteres Problem von Spielen, ebenfalls damaliger Zeitgeist, der sich hartnäckig hält: verschiedene Arten, einen Level zu spielen, also verschiedene Pfade mit unterschiedlichen Herausforderungen. Wie ich es hasse. Ich will ein Spiel einmal durchspielen und dabei alles gesehen haben. Alles andere ist vom Teufel.

Grafiken und Bewegungen wurden wieder verbessert, der technische Fortschritt halt. Dennoch ist auch dieser Teil eine technisch grundsolide Sache ohne Bugs.

Wenngleich viele Dinge eingebaut wurde, die ich für große Fehler bei Computerspielen halte, ist das Spiel dennoch eins der besten Tomb Raider. Die negativen Aspekte halten sich in Grenzen, die Rätsel wurden erweitert und komplexer gestaltet, die Locations sind überragend und das Spiel ist ein Paradebeispiel für Spielbarkeit.

Hier ist eine Entwicklung zu sehen, die gut ist: neue Dinge werden zwar eingeführt, dominieren jedoch das Spiel nicht, so dass die Grundidee erhalten bleibt und wirklich behutsam ergänzt wird.

Das Spiel endet mit dem Tod von Lara, denn die Entwickler:innen hatten genug von der Figur. Dass das natürlich nicht das Ende der Spiele sein wird, war jedem klar, die Frage war nur: wann kommt ein neues Spiel raus und wie wird es gestaltet?

Fazit: eins der besten Tomb Raider trotz größerer Änderungen.

Kategorien
Computerspiele Kritik

Spielkritik: Tomb Raider III (1998)

Wenn etwas funktioniert, gibt es Nachfolger bis zum Abwinken. Jedes Jahr ein neues Tomb Raider, diesmal Tomb Raider III, wieder Core Design, verbesserte Game Engine – es ist also wieder mehr vom Gleichen in Schöner zu erwarten.

Und glücklicherweise wurde ich nicht enttäuscht, graphisch deutlich verbessert, kann Lara jetzt sprinten und an der Decke hangeln, es gibt eine Desert Eagle mit unglaublicher Durchschlagskraft und Rätsel, Rätsel, Rätsel. Wunderschön.

Beim Leveldesign wurde großer Wert auf Abwechslung gelegt, leider mit einem „keine-Waffen-Level“, eine Sache, die ich bei keinem Spiel mag. Sei es drum, der Rest ist gelungen: Indien, Nevada, Area 51, London, Südsee und Antarktis – jedes Level mit eigenen Herausforderungen und schönen Momenten. Erfreulicherweise wurde gegenüber dem Vorgänger der Schießanteil heruntergefahren und der Rätselanteil erhöht.

Was ich gut fand: das gleiche Spiel in neuen Locations spielen zu können. Leider begann das, Kritiker allmählich zu stören, die gerne Innovation haben wollten und nicht wieder das gleiche Spiel mit leichten Verbesserungen.

Wie gesagt, ich bin da anderer Meinung: wer ein anderes Spiel will, soll sich halt ein anderes kaufen, auch damals gab es da genügend Auswahl.

Die mittleren Level konnten in beliebiger Reihenfolge gespielt werden – ein Riesending was mir persönlich egal ist, da ich auch mit festgelegten Reihenfolgen gut leben kann.

Die Automatikpistolen sind weg – WTF?

Es fanden sich genügend Käufer:innen, um aus dem Spiel einen riesigen Erfolg zu machen.

Fazit: wieder ein sehr guter Nachfolger mit deutlichem Gewicht auf Rätseln – genau mein Ding.

Kategorien
Computerspiele Kritik

Spielkritik: Tomb Raider II (1997)

Ein Jahr nach dem phänomenalen Tomb Raider jetzt Tomb Raider II von Core Design – würde es die Qualität halten können?

Ja. Und noch besser: es hat wenig geändert und das wenige zum Guten. So kamen ein paar Waffen dazu, man konnte verschiedene Fahrzeuge fahren, Lara bekam ihren Zopf und eine geringere Oberweite (wie in jedem Spiel, bis sie jetzt ganz normal aussieht). Negativ: die Unterwasserpassagen wurden ausgedehnt, das ist nicht so meins.

Die Level blieben exotisch, wurden aber diverser: Große Mauer, Venedig, Bohrinsel, Tibet, England… Diesmal mit angepasster Kleidung.

Beste Neuerung: Lara kann im Sprung eine Rolle machen. Klingt jetzt nach wenig, hat mich aber beeindruckt.

Leveldesign ist wieder auf hohem, guten Niveau, Schwierigkeitsgrad angemessen, auch Tomb Raider II ist eine gutes, gut spielbares Spiel. Der Vorgänger wurde behutsam weiterentwickelt, blieb aber dem ersten Teil treu. Der Trainingslevel war beeindruckend.

Im Unterschied zu heutigen Rezensionen wurde das auch sehr positiv aufgenommen: im Prinzip das gleiche Spiel noch einmal mit anderen Leveln und ohne große Neuerungen – das war was Gutes.

Anders gesagt: wer das erste Tomb Raider mochte, mochte auch das zweite, weil nicht viel verändert wurde. Das ist für mich das Wesen einer Spielereihe, in den letzten zehn Jahren wurde durch sehr satte Redakteure und eine kleine Gruppe von Dauerspielern (absichtlich nicht gegendert) eine andere Linie bevorzugt: Innovation und Verschärfung des Schwierigkeitsgrades in jedem neuen Teil, bis irgendwann mal klar wurde, dass das für professionelle Spieler wichtig sein kann, für den Rest der Welt aber nicht – der Rest bringt aber das Geld. Assassin’s-Creed-Spieler:innen können da leidgeplagt von berichten, wie schade es ist, wenn sich eine Reihe bis zur Unspielbarkeit von ihrem Ursprung entfernt.

Ja, auch Tomb-Raider-Spieler:innen können davon berichten und das Reboot ist weit vom Ursprung entfernt, macht aber auch einiges richtig – Mindestanforderung: es ist spielbar auch für mich.

Fazit: würdige, sehr gute Fortsetzung.

Kategorien
Computerspiele Kritik

Spielkritik: Tomb Raider (1996)

Tomb Raider – der Beginn einer größeren Spieleserie, die bis heute Ableger produziert – Spiele, Bücher, Filme, wahrscheinlich auch noch viel mehr. Lara wurde erdacht von Toby Gard, Hersteller des Spiels: Core Design, eine englische Firma (damals nicht ungewöhnlich im Spielegenre), Teil von Eidos Interactive.

Doom war schon draußen, es gab also schon 3D-Spiele und mit Doom war der ultimative 3D-Shooter gleich mit dem ersten Spiel schon erledigt.

Tomb Raider erledigte das für 3D-Abenteuerspiele (Abenteuer im Indiana-Jones-Sinn): gleich das erste Spiel definierte das Genre, definierte die Spielmechanik und war so gut, dass nur Details zu verbessern waren, aber das Spielprinzip über lange Zeit bis hin zu z.B. Uncharted gültig blieb.

Wir spielen Lara in der 3rd-Person-Perspektive auf der Suche nach dem Scion, einem alten Artefakt. Dazu reisen wir nach Peru (wo bereits der ikonische Dinosaurier wartet), dann zur Natla-Corporation über St. Francis’ Folly nach Ägypten, wo der große Showdown in der Pyramide wartet.

Es ist heute schwer nachzuvollziehen, wie gewaltig diese Spielewelt war, wie befreiend, sich als Lara bewegen zu können, völlig frei in Interaktion mit der Umwelt! Dabei waren Bewegungen und Kampfsystem äußerst durchdacht, wie schön z.B., dass die Waffen immer automatisch auf einen Feind zielten. Und die Bewegungen waren gut designt, teils roh, teils anmutig, Steine schieben, klettern, hangeln, schwimmen, Kopfsprung – alles drin.

Dazu ein geniales Leveldesign, dass die Level nicht zu schlauchig machte, aber auch nicht so offen, dass man sich verirren konnte. Aufgaben, die klar definiert waren und doch einiges Nachdenken oder Fingerfertigkeit erforderten. Das alles bei einem Schrierigkeitsgrad, der genau richtig war.

Voice-Acting war auch in Ordnung (ich hatte damals für DOOM 2 eine Soundkarte gekauft, die grundsätzlich in Spielen sehr hilfreich sein sollte) und einprägsame Sounds und Erkennungsmelodien.

Es gibt tatsächlich nicht viel am Spiel auszusetzen: Design, Ideen, technische Qualität – hier hat alles gestimmt.

Und, was im Gegensatz zu heutigen Spielen auffällt: es ist ein einfach zu spielendes Spiel bezüglich der Tastenkombinationen und Nicht-Spiel-Elemente, die zu beherrschen sind. Kein kompliziertes Aufleveln, keine unnötiges Nachladen, automatische Gegneranzielung – das alles hilft bei der Konzentration auf das Spiel. (Gilt für die meisten Spiele dieser Zeit, z.B. auch DOOM und Nachfolger.)

Heutzutage erwarten Spieler:innen offensichtlich mehr, da müssen Waffen gelevelt werden, Fähigkeiten freigespielt und das alles außerhalb des Spiels, was ich persönlich nicht gut finde.

Ich bin eine Archölogin, die ein Artefakt aus Atlantis sucht und am Ende gegen eine Dämonin kämpft und da stört Leute ernsthaft, dass ich mit den Pistolen ohne Nachladen unendlich schießen kann?

Sei es drum, heutzutage sollte man das Spiel nur spielen, wenn man von den Klötzchengrafiken abstrahieren und sich auch daran erfreuen kann. Das Spielprinzip und Spieldesign ist gut gealtert, die Grafik, Bewegungen etc. nicht.

Fazit: genredefinierender Meilenstein, der alles richtig macht.