fixed receiving responses from score server master 4.3.0.2
authorHolger Schemel <info@artsoft.org>
Wed, 1 Dec 2021 11:21:12 +0000 (12:21 +0100)
committerHolger Schemel <info@artsoft.org>
Wed, 1 Dec 2021 11:21:12 +0000 (12:21 +0100)
This commit fixes problems with receiving (slightly) larger responses
from the score server, causing high score lists with many entries to
be cut off after a few dozen entries.

This bug was caused by misleading documentation of SDL_net function
"SDLNet_TCP_Recv()", which claims to "wait until the full requested
length is sent", which unfortunately is not correct. Instead, data
sent from the server has to be polled until everything is completely
transmitted (using the "Content-Length" field in the HTTP header).

46 files changed:
Makefile
build-projects/android/build-scripts/create_sdl.sh
graphics/gfx_classic/RocksCollect.png [new file with mode: 0644]
src/Android.mk
src/Makefile
src/anim.c
src/conf_gfx.c
src/config.c
src/config.h
src/editor.c
src/events.c
src/files.c
src/files.h
src/game.c
src/game.h
src/game_mm/mm_game.c
src/game_mm/mm_game.h
src/game_mm/mm_main.h
src/game_mm/mm_tools.c
src/init.c
src/libgame/Makefile
src/libgame/base64.c [new file with mode: 0644]
src/libgame/base64.h [new file with mode: 0644]
src/libgame/gadgets.c
src/libgame/http.c [new file with mode: 0644]
src/libgame/http.h [new file with mode: 0644]
src/libgame/image.h
src/libgame/libgame.h
src/libgame/misc.c
src/libgame/misc.h
src/libgame/platform.h
src/libgame/setup.c
src/libgame/setup.h
src/libgame/sound.c
src/libgame/system.c
src/libgame/system.h
src/libgame/text.c
src/libgame/text.h
src/main.c
src/main.h
src/screens.c
src/screens.h
src/tape.c
src/tape.h
src/tools.c
src/tools.h

index 5dd17c2..69fbf22 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -21,19 +21,12 @@ CC = gcc
 # (this must be set to "gmake" for some systems)
 MAKE = make
 
-# directory for read-only game data (like graphics, sounds, levels)
+# directory for (read-only) game data (like graphics, sounds, levels)
 # (this directory is usually the game's installation directory)
 # default is '.' to be able to run program without installation
-# RO_GAME_DIR = .
+# BASE_PATH = .
 # use the following setting for Debian / Ubuntu installations:
-# RO_GAME_DIR = /usr/share/games/rocksndiamonds
-
-# directory for writable game data (like highscore files)
-# (if no "scores" directory exists, scores are saved in user data directory)
-# default is '.' to be able to run program without installation
-# RW_GAME_DIR = .
-# use the following setting for Debian / Ubuntu installations:
-# RW_GAME_DIR = /var/games/rocksndiamonds
+# BASE_PATH = /usr/share/games/rocksndiamonds
 
 # uncomment if system has no joystick include file
 # JOYSTICK = -DNO_JOYSTICK
@@ -96,7 +89,7 @@ clean-android: android-clean
 # development targets
 # -----------------------------------------------------------------------------
 
-MAKE_ENGINETEST = ./Scripts/make_enginetest.sh
+MAKE_ENGINETEST = ./tests/enginetest/enginetest.sh
 MAKE_LEVELSKETCH = ./Scripts/make_levelsketch_images.sh
 
 auto-conf:
@@ -126,18 +119,6 @@ depend dep:
 enginetest: all
        $(MAKE_ENGINETEST)
 
-enginetestcustom: all
-       $(MAKE_ENGINETEST) custom
-
-enginetestfast: all
-       $(MAKE_ENGINETEST) fast
-
-enginetestnew: all
-       $(MAKE_ENGINETEST) new
-
-leveltest: all
-       $(MAKE_ENGINETEST) leveltest
-
 levelsketch_images: all
        $(MAKE_LEVELSKETCH)
 
@@ -187,6 +168,9 @@ dist-package-mac:
 dist-package-android:
        $(MAKE_DIST) package android
 
+dist-package-emscripten:
+       $(MAKE_DIST) package emscripten
+
 dist-copy-package-linux:
        $(MAKE_DIST) copy-package linux
 
@@ -202,6 +186,9 @@ dist-copy-package-mac:
 dist-copy-package-android:
        $(MAKE_DIST) copy-package android
 
+dist-copy-package-emscripten:
+       $(MAKE_DIST) copy-package emscripten
+
 dist-upload-linux:
        $(MAKE_DIST) upload linux
 
@@ -217,12 +204,19 @@ dist-upload-mac:
 dist-upload-android:
        $(MAKE_DIST) upload android
 
+dist-upload-emscripten:
+       $(MAKE_DIST) upload emscripten
+
+dist-deploy-emscripten:
+       $(MAKE_DIST) deploy emscripten
+
 dist-package-all:
        $(MAKE) dist-package-linux
        $(MAKE) dist-package-win32
        $(MAKE) dist-package-win64
        $(MAKE) dist-package-mac
        $(MAKE) dist-package-android
+       $(MAKE) dist-package-emscripten
 
 dist-copy-package-all:
        $(MAKE) dist-copy-package-linux
@@ -230,6 +224,7 @@ dist-copy-package-all:
        $(MAKE) dist-copy-package-win64
        $(MAKE) dist-copy-package-mac
        $(MAKE) dist-copy-package-android
+       $(MAKE) dist-copy-package-emscripten
 
 dist-upload-all:
        $(MAKE) dist-upload-linux
@@ -237,6 +232,10 @@ dist-upload-all:
        $(MAKE) dist-upload-win64
        $(MAKE) dist-upload-mac
        $(MAKE) dist-upload-android
+       $(MAKE) dist-upload-emscripten
+
+dist-deploy-all:
+       $(MAKE) dist-deploy-emscripten
 
 dist-release-all: dist-package-all dist-copy-package-all dist-upload-all
 
@@ -246,4 +245,6 @@ copy-package-all: dist-copy-package-all
 
 upload-all: dist-upload-all
 
+deploy-all: dist-deploy-all
+
 release-all: dist-release-all
index 901d150..5ec679e 100755 (executable)
@@ -5,7 +5,8 @@ JNI_DIR="app/jni"
 ANDROID_MK_SDL_IMAGE="$JNI_DIR/SDL2_image/Android.mk"
 ANDROID_MK_SDL_MIXER="$JNI_DIR/SDL2_mixer/Android.mk"
 
-SDL_BASE_URL="https://www.libsdl.org"
+SDL_BASE_URL_ORIGINAL="https://www.libsdl.org"
+SDL_BASE_URL_FALLBACK="https://www.artsoft.org"
 SDL_VERSIONS=`cat SDL_VERSIONS`
 
 for i in $SDL_VERSIONS; do
@@ -22,13 +23,21 @@ for i in $SDL_VERSIONS; do
        SDL_RELEASE_DIR="projects/$SDL_SUBURL/release"
     fi
 
-    SDL_URL="$SDL_BASE_URL/$SDL_RELEASE_DIR/$i.tar.gz"
+    SDL_URL="$SDL_BASE_URL_ORIGINAL/$SDL_RELEASE_DIR/$i.tar.gz"
 
-    wget -O - "$SDL_URL" | (cd "$JNI_DIR" && tar xzf -)
+    wget --timeout=10 -O - "$SDL_URL" | (cd "$JNI_DIR" && tar xzf -)
 
     if [ "$?" != "0" ]; then
-       echo "ERROR: Installing '$i' failed!"
-       exit 10
+       echo "ERROR: Installing '$i' from main site failed -- trying fallback!"
+
+       SDL_URL="$SDL_BASE_URL_FALLBACK/RELEASES/sdl/$i.tar.gz"
+
+       wget --timeout=10 -O - "$SDL_URL" | (cd "$JNI_DIR" && tar xzf -)
+
+       if [ "$?" != "0" ]; then
+           echo "ERROR: Installing '$i' from fallback site failed!"
+           exit 10
+       fi
     fi
 
     mv "$JNI_DIR/$i" "$JNI_DIR/$SDL_SUBDIR"
diff --git a/graphics/gfx_classic/RocksCollect.png b/graphics/gfx_classic/RocksCollect.png
new file mode 100644 (file)
index 0000000..e5a72bf
Binary files /dev/null and b/graphics/gfx_classic/RocksCollect.png differ
index 0652311..5174150 100644 (file)
@@ -46,6 +46,8 @@ LOCAL_SRC_FILES := $(SDL_PATH)/src/main/android/SDL_android_main.c \
        libgame/image.c                 \
        libgame/random.c                \
        libgame/hash.c                  \
+       libgame/http.c                  \
+       libgame/base64.c                \
        libgame/setup.c                 \
        libgame/misc.c                  \
        libgame/sdl.c                   \
index f40ada2..0456b90 100644 (file)
@@ -100,7 +100,7 @@ ifeq ($(PLATFORM),emscripten)
 SDL_LIBS = -s USE_SDL_IMAGE=2 -s USE_SDL_MIXER=2 -s USE_SDL_NET=2 -s USE_ZLIB=1
 SDL_FMTS = -s SDL2_IMAGE_FORMATS='["bmp","png","pcx","xpm"]'
 EXTRA_CFLAGS = $(SDL_LIBS)
-EXTRA_LDFLAGS = $(SDL_FMTS) -s INITIAL_MEMORY=81920000 -s ALLOW_MEMORY_GROWTH=1 --preload-file ../graphics/ --preload-file ../sounds/ --preload-file ../levels/ --preload-file ../music/ -s NO_EXIT_RUNTIME=0 -s ASYNCIFY -O2 -lidbfs.js
+EXTRA_LDFLAGS = $(SDL_FMTS) -s INITIAL_MEMORY=81920000 -s ALLOW_MEMORY_GROWTH=1 --preload-file ../conf/ --preload-file ../docs/ --preload-file ../levels/ --preload-file ../graphics/ --preload-file ../sounds/ --preload-file ../music/ -s NO_EXIT_RUNTIME=0 -s ASYNCIFY -O2 -lidbfs.js
 else
 SDL_LIBS = -lSDL2_image -lSDL2_mixer -lSDL2_net
 endif
@@ -114,15 +114,11 @@ endif
 # configuring compile-time definitions
 # -----------------------------------------------------------------------------
 
-ifdef RO_GAME_DIR                      # path to read-only game data specified
-CONFIG_RO_GAME_DIR = -DRO_GAME_DIR="\"$(RO_GAME_DIR)\""
+ifdef BASE_PATH                                        # path to read-only game data
+CONFIG_BASE_PATH = -DBASE_PATH="\"$(BASE_PATH)\""
 endif
 
-ifdef RW_GAME_DIR                      # path to writable game data specified
-CONFIG_RW_GAME_DIR = -DRW_GAME_DIR="\"$(RW_GAME_DIR)\""
-endif
-
-CONFIG = $(CONFIG_RO_GAME_DIR) $(CONFIG_RW_GAME_DIR) $(JOYSTICK)
+CONFIG = $(CONFIG_BASE_PATH) $(JOYSTICK)
 
 DEBUG = -DDEBUG -g
 
@@ -137,6 +133,10 @@ OPTIONS = $(DEBUG) -Wall -Wstrict-prototypes -Wmissing-prototypes
 # OPTIONS = -O2 -Wall
 # OPTIONS = -O2
 
+ifdef BUILD_TEST                       # test build
+OPTIONS := $(OPTIONS) -DTESTING
+endif
+
 ifdef BUILD_DIST                       # distribution build
 SYS_LDFLAGS := $(shell echo $(SYS_LDFLAGS) |   \
                       sed -e "s%-rpath,[^ ]*%-rpath,'\$$ORIGIN/lib'%")
index 2e6280c..c2ec75f 100644 (file)
@@ -825,8 +825,8 @@ static void DrawGlobalAnimationsExt(int drawing_target, int drawing_stage)
 
        gfx.anim_random_frame = last_anim_random_frame;
 
-       getFixedGraphicSource(part->graphic, frame, &src_bitmap,
-                             &src_x, &src_y);
+       getGlobalAnimGraphicSource(part->graphic, frame, &src_bitmap,
+                                  &src_x, &src_y);
 
        src_x += cut_x;
        src_y += cut_y;
index 343c272..96d500c 100644 (file)
@@ -153,6 +153,11 @@ struct ConfigInfo image_config[] =
   { "bd_diamond.falling.ypos",                 "10"                    },
   { "bd_diamond.falling.frames",               "2"                     },
   { "bd_diamond.falling.delay",                        "4"                     },
+  { "bd_diamond.collecting",                   "RocksCollect.png"      },
+  { "bd_diamond.collecting.xpos",              "0"                     },
+  { "bd_diamond.collecting.ypos",              "8"                     },
+  { "bd_diamond.collecting.frames",            "7"                     },
+  { "bd_diamond.collecting.anim_mode",         "linear"                },
 
   { "bd_magic_wall",                           "RocksElements.png"     },
   { "bd_magic_wall.xpos",                      "12"                    },
@@ -979,11 +984,10 @@ struct ConfigInfo image_config[] =
   { "emerald.falling.ypos",                    "0"                     },
   { "emerald.falling.frames",                  "2"                     },
   { "emerald.falling.delay",                   "4"                     },
-  { "emerald.collecting",                      "RocksMore.png"         },
-  { "emerald.collecting.xpos",                 "3"                     },
-  { "emerald.collecting.ypos",                 "2"                     },
-  { "emerald.collecting.frames",               "3"                     },
-  { "emerald.collecting.delay",                        "2"                     },
+  { "emerald.collecting",                      "RocksCollect.png"      },
+  { "emerald.collecting.xpos",                 "0"                     },
+  { "emerald.collecting.ypos",                 "0"                     },
+  { "emerald.collecting.frames",               "7"                     },
   { "emerald.collecting.anim_mode",            "linear"                },
 
   { "diamond",                                 "RocksElements.png"     },
@@ -1000,11 +1004,10 @@ struct ConfigInfo image_config[] =
   { "diamond.falling.ypos",                    "0"                     },
   { "diamond.falling.frames",                  "2"                     },
   { "diamond.falling.delay",                   "4"                     },
-  { "diamond.collecting",                      "RocksMore.png"         },
-  { "diamond.collecting.xpos",                 "7"                     },
-  { "diamond.collecting.ypos",                 "2"                     },
-  { "diamond.collecting.frames",               "3"                     },
-  { "diamond.collecting.delay",                        "2"                     },
+  { "diamond.collecting",                      "RocksCollect.png"      },
+  { "diamond.collecting.xpos",                 "0"                     },
+  { "diamond.collecting.ypos",                 "1"                     },
+  { "diamond.collecting.frames",               "7"                     },
   { "diamond.collecting.anim_mode",            "linear"                },
 
   { "bomb",                                    "RocksElements.png"     },
@@ -1033,6 +1036,11 @@ struct ConfigInfo image_config[] =
   { "dynamite.active.frames",                  "7"                     },
   { "dynamite.active.delay",                   "12"                    },
   { "dynamite.active.anim_mode",               "linear"                },
+  { "dynamite.collecting",                     "RocksCollect.png"      },
+  { "dynamite.collecting.xpos",                        "0"                     },
+  { "dynamite.collecting.ypos",                        "7"                     },
+  { "dynamite.collecting.frames",              "7"                     },
+  { "dynamite.collecting.anim_mode",           "linear"                },
 
   { "em_dynamite",                             "RocksEMC.png"          },
   { "em_dynamite.xpos",                                "0"                     },
@@ -1047,6 +1055,11 @@ struct ConfigInfo image_config[] =
   { "em_dynamite.active.EDITOR",               "RocksEMC.png"          },
   { "em_dynamite.active.EDITOR.xpos",          "2"                     },
   { "em_dynamite.active.EDITOR.ypos",          "15"                    },
+  { "em_dynamite.collecting",                  "RocksCollect.png"      },
+  { "em_dynamite.collecting.xpos",             "0"                     },
+  { "em_dynamite.collecting.ypos",             "15"                    },
+  { "em_dynamite.collecting.frames",           "7"                     },
+  { "em_dynamite.collecting.anim_mode",                "linear"                },
 
   { "wall_emerald",                            "RocksElements.png"     },
   { "wall_emerald.xpos",                       "4"                     },
@@ -1522,23 +1535,48 @@ struct ConfigInfo image_config[] =
   { "em_key_1.xpos",                           "4"                     },
   { "em_key_1.ypos",                           "6"                     },
   { "em_key_1.frames",                         "1"                     },
+  { "em_key_1.collecting",                     "RocksCollect.png"      },
+  { "em_key_1.collecting.xpos",                        "7"                     },
+  { "em_key_1.collecting.ypos",                        "4"                     },
+  { "em_key_1.collecting.frames",              "7"                     },
+  { "em_key_1.collecting.anim_mode",           "linear"                },
   { "em_key_2",                                        "RocksSP.png"           },
   { "em_key_2.xpos",                           "5"                     },
   { "em_key_2.ypos",                           "6"                     },
   { "em_key_2.frames",                         "1"                     },
+  { "em_key_2.collecting",                     "RocksCollect.png"      },
+  { "em_key_2.collecting.xpos",                        "7"                     },
+  { "em_key_2.collecting.ypos",                        "5"                     },
+  { "em_key_2.collecting.frames",              "7"                     },
+  { "em_key_2.collecting.anim_mode",           "linear"                },
   { "em_key_3",                                        "RocksSP.png"           },
   { "em_key_3.xpos",                           "6"                     },
   { "em_key_3.ypos",                           "6"                     },
   { "em_key_3.frames",                         "1"                     },
+  { "em_key_3.collecting",                     "RocksCollect.png"      },
+  { "em_key_3.collecting.xpos",                        "7"                     },
+  { "em_key_3.collecting.ypos",                        "6"                     },
+  { "em_key_3.collecting.frames",              "7"                     },
+  { "em_key_3.collecting.anim_mode",           "linear"                },
   { "em_key_4",                                        "RocksSP.png"           },
   { "em_key_4.xpos",                           "7"                     },
   { "em_key_4.ypos",                           "6"                     },
   { "em_key_4.frames",                         "1"                     },
+  { "em_key_4.collecting",                     "RocksCollect.png"      },
+  { "em_key_4.collecting.xpos",                        "7"                     },
+  { "em_key_4.collecting.ypos",                        "7"                     },
+  { "em_key_4.collecting.frames",              "7"                     },
+  { "em_key_4.collecting.anim_mode",           "linear"                },
 
   { "dc_key_white",                            "RocksSP.png"           },
   { "dc_key_white.xpos",                       "13"                    },
   { "dc_key_white.ypos",                       "1"                     },
   { "dc_key_white.frames",                     "1"                     },
+  { "dc_key_white.collecting",                 "RocksCollect.png"      },
+  { "dc_key_white.collecting.xpos",            "7"                     },
+  { "dc_key_white.collecting.ypos",            "0"                     },
+  { "dc_key_white.collecting.frames",          "7"                     },
+  { "dc_key_white.collecting.anim_mode",       "linear"                },
 
   { "em_gate_1",                               "RocksSP.png"           },
   { "em_gate_1.xpos",                          "0"                     },
@@ -2118,41 +2156,37 @@ struct ConfigInfo image_config[] =
   { "envelope_1.xpos",                         "0"                     },
   { "envelope_1.ypos",                         "4"                     },
   { "envelope_1.frames",                       "1"                     },
-  { "envelope_1.collecting",                   "RocksMore.png"         },
-  { "envelope_1.collecting.xpos",              "5"                     },
-  { "envelope_1.collecting.ypos",              "4"                     },
-  { "envelope_1.collecting.frames",            "3"                     },
-  { "envelope_1.collecting.delay",             "2"                     },
+  { "envelope_1.collecting",                   "RocksCollect.png"      },
+  { "envelope_1.collecting.xpos",              "7"                     },
+  { "envelope_1.collecting.ypos",              "8"                     },
+  { "envelope_1.collecting.frames",            "7"                     },
   { "envelope_1.collecting.anim_mode",         "linear"                },
   { "envelope_2",                              "RocksMore.png"         },
   { "envelope_2.xpos",                         "1"                     },
   { "envelope_2.ypos",                         "4"                     },
   { "envelope_2.frames",                       "1"                     },
-  { "envelope_2.collecting",                   "RocksMore.png"         },
-  { "envelope_2.collecting.xpos",              "5"                     },
-  { "envelope_2.collecting.ypos",              "4"                     },
-  { "envelope_2.collecting.frames",            "3"                     },
-  { "envelope_2.collecting.delay",             "2"                     },
+  { "envelope_2.collecting",                   "RocksCollect.png"      },
+  { "envelope_2.collecting.xpos",              "7"                     },
+  { "envelope_2.collecting.ypos",              "9"                     },
+  { "envelope_2.collecting.frames",            "7"                     },
   { "envelope_2.collecting.anim_mode",         "linear"                },
   { "envelope_3",                              "RocksMore.png"         },
   { "envelope_3.xpos",                         "2"                     },
   { "envelope_3.ypos",                         "4"                     },
   { "envelope_3.frames",                       "1"                     },
-  { "envelope_3.collecting",                   "RocksMore.png"         },
-  { "envelope_3.collecting.xpos",              "5"                     },
-  { "envelope_3.collecting.ypos",              "4"                     },
-  { "envelope_3.collecting.frames",            "3"                     },
-  { "envelope_3.collecting.delay",             "2"                     },
+  { "envelope_3.collecting",                   "RocksCollect.png"      },
+  { "envelope_3.collecting.xpos",              "7"                     },
+  { "envelope_3.collecting.ypos",              "10"                    },
+  { "envelope_3.collecting.frames",            "7"                     },
   { "envelope_3.collecting.anim_mode",         "linear"                },
   { "envelope_4",                              "RocksMore.png"         },
   { "envelope_4.xpos",                         "3"                     },
   { "envelope_4.ypos",                         "4"                     },
   { "envelope_4.frames",                       "1"                     },
-  { "envelope_4.collecting",                   "RocksMore.png"         },
-  { "envelope_4.collecting.xpos",              "5"                     },
-  { "envelope_4.collecting.ypos",              "4"                     },
-  { "envelope_4.collecting.frames",            "3"                     },
-  { "envelope_4.collecting.delay",             "2"                     },
+  { "envelope_4.collecting",                   "RocksCollect.png"      },
+  { "envelope_4.collecting.xpos",              "7"                     },
+  { "envelope_4.collecting.ypos",              "11"                    },
+  { "envelope_4.collecting.frames",            "7"                     },
   { "envelope_4.collecting.anim_mode",         "linear"                },
 
   { "sign_radioactivity",                      "RocksDC.png"           },
@@ -2277,6 +2311,11 @@ struct ConfigInfo image_config[] =
   { "extra_time.ypos",                         "0"                     },
   { "extra_time.frames",                       "6"                     },
   { "extra_time.delay",                                "4"                     },
+  { "extra_time.collecting",                   "RocksCollect.png"      },
+  { "extra_time.collecting.xpos",              "7"                     },
+  { "extra_time.collecting.ypos",              "2"                     },
+  { "extra_time.collecting.frames",            "7"                     },
+  { "extra_time.collecting.anim_mode",         "linear"                },
 
   { "shield_normal",                           "RocksDC.png"           },
   { "shield_normal.xpos",                      "8"                     },
@@ -2289,6 +2328,11 @@ struct ConfigInfo image_config[] =
   { "shield_normal.active.frames",             "3"                     },
   { "shield_normal.active.delay",              "8"                     },
   { "shield_normal.active.anim_mode",          "pingpong"              },
+  { "shield_normal.collecting",                        "RocksCollect.png"      },
+  { "shield_normal.collecting.xpos",           "7"                     },
+  { "shield_normal.collecting.ypos",           "1"                     },
+  { "shield_normal.collecting.frames",         "7"                     },
+  { "shield_normal.collecting.anim_mode",      "linear"                },
 
   { "shield_deadly",                           "RocksDC.png"           },
   { "shield_deadly.xpos",                      "8"                     },
@@ -2301,6 +2345,11 @@ struct ConfigInfo image_config[] =
   { "shield_deadly.active.frames",             "3"                     },
   { "shield_deadly.active.delay",              "8"                     },
   { "shield_deadly.active.anim_mode",          "pingpong"              },
+  { "shield_deadly.collecting",                        "RocksCollect.png"      },
+  { "shield_deadly.collecting.xpos",           "7"                     },
+  { "shield_deadly.collecting.ypos",           "3"                     },
+  { "shield_deadly.collecting.frames",         "7"                     },
+  { "shield_deadly.collecting.anim_mode",      "linear"                },
 
   { "switchgate_closed",                       "RocksDC.png"           },
   { "switchgate_closed.xpos",                  "8"                     },
@@ -2352,11 +2401,21 @@ struct ConfigInfo image_config[] =
   { "pearl.breaking.frames",                   "4"                     },
   { "pearl.breaking.delay",                    "2"                     },
   { "pearl.breaking.anim_mode",                        "linear"                },
+  { "pearl.collecting",                                "RocksCollect.png"      },
+  { "pearl.collecting.xpos",                   "0"                     },
+  { "pearl.collecting.ypos",                   "16"                    },
+  { "pearl.collecting.frames",                 "7"                     },
+  { "pearl.collecting.anim_mode",              "linear"                },
 
   { "crystal",                                 "RocksDC.png"           },
   { "crystal.xpos",                            "9"                     },
   { "crystal.ypos",                            "11"                    },
   { "crystal.frames",                          "1"                     },
+  { "crystal.collecting",                      "RocksCollect.png"      },
+  { "crystal.collecting.xpos",                 "0"                     },
+  { "crystal.collecting.ypos",                 "17"                    },
+  { "crystal.collecting.frames",               "7"                     },
+  { "crystal.collecting.anim_mode",            "linear"                },
 
   { "wall_pearl",                              "RocksDC.png"           },
   { "wall_pearl.xpos",                         "10"                    },
@@ -2540,18 +2599,38 @@ struct ConfigInfo image_config[] =
   { "key_1.xpos",                              "4"                     },
   { "key_1.ypos",                              "1"                     },
   { "key_1.frames",                            "1"                     },
+  { "key_1.collecting",                                "RocksCollect.png"      },
+  { "key_1.collecting.xpos",                   "0"                     },
+  { "key_1.collecting.ypos",                   "3"                     },
+  { "key_1.collecting.frames",                 "7"                     },
+  { "key_1.collecting.anim_mode",              "linear"                },
   { "key_2",                                   "RocksElements.png"     },
   { "key_2.xpos",                              "5"                     },
   { "key_2.ypos",                              "1"                     },
   { "key_2.frames",                            "1"                     },
+  { "key_2.collecting",                                "RocksCollect.png"      },
+  { "key_2.collecting.xpos",                   "0"                     },
+  { "key_2.collecting.ypos",                   "4"                     },
+  { "key_2.collecting.frames",                 "7"                     },
+  { "key_2.collecting.anim_mode",              "linear"                },
   { "key_3",                                   "RocksElements.png"     },
   { "key_3.xpos",                              "6"                     },
   { "key_3.ypos",                              "1"                     },
   { "key_3.frames",                            "1"                     },
+  { "key_3.collecting",                                "RocksCollect.png"      },
+  { "key_3.collecting.xpos",                   "0"                     },
+  { "key_3.collecting.ypos",                   "5"                     },
+  { "key_3.collecting.frames",                 "7"                     },
+  { "key_3.collecting.anim_mode",              "linear"                },
   { "key_4",                                   "RocksElements.png"     },
   { "key_4.xpos",                              "7"                     },
   { "key_4.ypos",                              "1"                     },
   { "key_4.frames",                            "1"                     },
+  { "key_4.collecting",                                "RocksCollect.png"      },
+  { "key_4.collecting.xpos",                   "0"                     },
+  { "key_4.collecting.ypos",                   "6"                     },
+  { "key_4.collecting.frames",                 "7"                     },
+  { "key_4.collecting.anim_mode",              "linear"                },
 
   { "gate_1",                                  "RocksElements.png"     },
   { "gate_1.xpos",                             "4"                     },
@@ -2701,6 +2780,11 @@ struct ConfigInfo image_config[] =
   { "emerald_yellow.falling.ypos",             "8"                     },
   { "emerald_yellow.falling.frames",           "2"                     },
   { "emerald_yellow.falling.delay",            "4"                     },
+  { "emerald_yellow.collecting",               "RocksCollect.png"      },
+  { "emerald_yellow.collecting.xpos",          "0"                     },
+  { "emerald_yellow.collecting.ypos",          "9"                     },
+  { "emerald_yellow.collecting.frames",                "7"                     },
+  { "emerald_yellow.collecting.anim_mode",     "linear"                },
   { "emerald_red",                             "RocksElements.png"     },
   { "emerald_red.xpos",                                "8"                     },
   { "emerald_red.ypos",                                "9"                     },
@@ -2715,6 +2799,11 @@ struct ConfigInfo image_config[] =
   { "emerald_red.falling.ypos",                        "9"                     },
   { "emerald_red.falling.frames",              "2"                     },
   { "emerald_red.falling.delay",               "4"                     },
+  { "emerald_red.collecting",                  "RocksCollect.png"      },
+  { "emerald_red.collecting.xpos",             "0"                     },
+  { "emerald_red.collecting.ypos",             "13"                    },
+  { "emerald_red.collecting.frames",           "7"                     },
+  { "emerald_red.collecting.anim_mode",                "linear"                },
   { "emerald_purple",                          "RocksElements.png"     },
   { "emerald_purple.xpos",                     "10"                    },
   { "emerald_purple.ypos",                     "9"                     },
@@ -2729,6 +2818,11 @@ struct ConfigInfo image_config[] =
   { "emerald_purple.falling.ypos",             "9"                     },
   { "emerald_purple.falling.frames",           "2"                     },
   { "emerald_purple.falling.delay",            "4"                     },
+  { "emerald_purple.collecting",               "RocksCollect.png"      },
+  { "emerald_purple.collecting.xpos",          "0"                     },
+  { "emerald_purple.collecting.ypos",          "14"                    },
+  { "emerald_purple.collecting.frames",                "7"                     },
+  { "emerald_purple.collecting.anim_mode",     "linear"                },
 
   { "wall_emerald_yellow",                     "RocksElements.png"     },
   { "wall_emerald_yellow.xpos",                        "8"                     },
@@ -2862,6 +2956,11 @@ struct ConfigInfo image_config[] =
   { "speed_pill.xpos",                         "14"                    },
   { "speed_pill.ypos",                         "9"                     },
   { "speed_pill.frames",                       "1"                     },
+  { "speed_pill.collecting",                   "RocksCollect.png"      },
+  { "speed_pill.collecting.xpos",              "0"                     },
+  { "speed_pill.collecting.ypos",              "2"                     },
+  { "speed_pill.collecting.frames",            "7"                     },
+  { "speed_pill.collecting.anim_mode",         "linear"                },
 
   { "dark_yamyam",                             "RocksElements.png"     },
   { "dark_yamyam.xpos",                                "8"                     },
@@ -2923,14 +3022,29 @@ struct ConfigInfo image_config[] =
   { "dynabomb_increase_number.xpos",           "12"                    },
   { "dynabomb_increase_number.ypos",           "11"                    },
   { "dynabomb_increase_number.frames",         "1"                     },
+  { "dynabomb_increase_number.collecting",     "RocksCollect.png"      },
+  { "dynabomb_increase_number.collecting.xpos",        "0"                     },
+  { "dynabomb_increase_number.collecting.ypos",        "10"                    },
+  { "dynabomb_increase_number.collecting.frames", "7"                  },
+  { "dynabomb_increase_number.collecting.anim_mode", "linear"          },
   { "dynabomb_increase_size",                  "RocksElements.png"     },
   { "dynabomb_increase_size.xpos",             "15"                    },
   { "dynabomb_increase_size.ypos",             "11"                    },
   { "dynabomb_increase_size.frames",           "1"                     },
+  { "dynabomb_increase_size.collecting",       "RocksCollect.png"      },
+  { "dynabomb_increase_size.collecting.xpos",  "0"                     },
+  { "dynabomb_increase_size.collecting.ypos",  "11"                    },
+  { "dynabomb_increase_size.collecting.frames",        "7"                     },
+  { "dynabomb_increase_size.collecting.anim_mode", "linear"            },
   { "dynabomb_increase_power",                 "RocksElements.png"     },
   { "dynabomb_increase_power.xpos",            "12"                    },
   { "dynabomb_increase_power.ypos",            "9"                     },
   { "dynabomb_increase_power.frames",          "1"                     },
+  { "dynabomb_increase_power.collecting",      "RocksCollect.png"      },
+  { "dynabomb_increase_power.collecting.xpos", "0"                     },
+  { "dynabomb_increase_power.collecting.ypos", "12"                    },
+  { "dynabomb_increase_power.collecting.frames", "7"                   },
+  { "dynabomb_increase_power.collecting.anim_mode", "linear"           },
 
   { "pig",                                     "RocksHeroes.png"       },
   { "pig.xpos",                                        "8"                     },
@@ -4002,18 +4116,38 @@ struct ConfigInfo image_config[] =
   { "emc_key_5.xpos",                          "0"                     },
   { "emc_key_5.ypos",                          "5"                     },
   { "emc_key_5.frames",                                "1"                     },
+  { "emc_key_5.collecting",                    "RocksCollect.png"      },
+  { "emc_key_5.collecting.xpos",               "7"                     },
+  { "emc_key_5.collecting.ypos",               "12"                    },
+  { "emc_key_5.collecting.frames",             "7"                     },
+  { "emc_key_5.collecting.anim_mode",          "linear"                },
   { "emc_key_6",                               "RocksEMC.png"          },
   { "emc_key_6.xpos",                          "1"                     },
   { "emc_key_6.ypos",                          "5"                     },
   { "emc_key_6.frames",                                "1"                     },
+  { "emc_key_6.collecting",                    "RocksCollect.png"      },
+  { "emc_key_6.collecting.xpos",               "7"                     },
+  { "emc_key_6.collecting.ypos",               "13"                    },
+  { "emc_key_6.collecting.frames",             "7"                     },
+  { "emc_key_6.collecting.anim_mode",          "linear"                },
   { "emc_key_7",                               "RocksEMC.png"          },
   { "emc_key_7.xpos",                          "2"                     },
   { "emc_key_7.ypos",                          "5"                     },
   { "emc_key_7.frames",                                "1"                     },
+  { "emc_key_7.collecting",                    "RocksCollect.png"      },
+  { "emc_key_7.collecting.xpos",               "7"                     },
+  { "emc_key_7.collecting.ypos",               "14"                    },
+  { "emc_key_7.collecting.frames",             "7"                     },
+  { "emc_key_7.collecting.anim_mode",          "linear"                },
   { "emc_key_8",                               "RocksEMC.png"          },
   { "emc_key_8.xpos",                          "3"                     },
   { "emc_key_8.ypos",                          "5"                     },
   { "emc_key_8.frames",                                "1"                     },
+  { "emc_key_8.collecting",                    "RocksCollect.png"      },
+  { "emc_key_8.collecting.xpos",               "7"                     },
+  { "emc_key_8.collecting.ypos",               "15"                    },
+  { "emc_key_8.collecting.frames",             "7"                     },
+  { "emc_key_8.collecting.anim_mode",          "linear"                },
 
   { "emc_gate_5",                              "RocksEMC.png"          },
   { "emc_gate_5.xpos",                         "0"                     },
@@ -4235,11 +4369,21 @@ struct ConfigInfo image_config[] =
   { "emc_lenses.xpos",                         "6"                     },
   { "emc_lenses.ypos",                         "4"                     },
   { "emc_lenses.frames",                       "1"                     },
+  { "emc_lenses.collecting",                   "RocksCollect.png"      },
+  { "emc_lenses.collecting.xpos",              "7"                     },
+  { "emc_lenses.collecting.ypos",              "16"                    },
+  { "emc_lenses.collecting.frames",            "7"                     },
+  { "emc_lenses.collecting.anim_mode",         "linear"                },
 
   { "emc_magnifier",                           "RocksEMC.png"          },
   { "emc_magnifier.xpos",                      "7"                     },
   { "emc_magnifier.ypos",                      "4"                     },
   { "emc_magnifier.frames",                    "1"                     },
+  { "emc_magnifier.collecting",                        "RocksCollect.png"      },
+  { "emc_magnifier.collecting.xpos",           "7"                     },
+  { "emc_magnifier.collecting.ypos",           "17"                    },
+  { "emc_magnifier.collecting.frames",         "7"                     },
+  { "emc_magnifier.collecting.anim_mode",      "linear"                },
 
   { "emc_wall_9",                              "RocksEMC.png"          },
   { "emc_wall_9.xpos",                         "10"                    },
index d50ba68..8a7a5c5 100644 (file)
@@ -47,6 +47,11 @@ char *getProgramVersionString(void)
   return program.version_string;
 }
 
+char *getProgramPlatformString(void)
+{
+  return PLATFORM_STRING;
+}
+
 char *getProgramInitString(void)
 {
   static char *program_init_string = NULL;
index 06ae39e..31e86b8 100644 (file)
@@ -19,6 +19,7 @@ char *getSourceHashString(void);
 char *getProgramTitleString(void);
 char *getProgramRealVersionString(void);
 char *getProgramVersionString(void);
+char *getProgramPlatformString(void);
 char *getProgramInitString(void);
 char *getConfigProgramTitleString(void);
 char *getConfigProgramCopyrightString(void);
index 2459059..282c530 100644 (file)
@@ -637,6 +637,7 @@ enum
   // checkbuttons/radiobuttons for level/element properties
 
   GADGET_ID_AUTO_COUNT_GEMS,
+  GADGET_ID_RATE_TIME_OVER_SCORE,
   GADGET_ID_USE_LEVELSET_ARTWORK,
   GADGET_ID_COPY_LEVEL_TEMPLATE,
   GADGET_ID_RANDOM_PERCENTAGE,
@@ -656,6 +657,7 @@ enum
   GADGET_ID_AUTO_EXIT_SOKOBAN,
   GADGET_ID_SOLVED_BY_ONE_PLAYER,
   GADGET_ID_FINISH_DIG_COLLECT,
+  GADGET_ID_KEEP_WALKABLE_CE,
   GADGET_ID_CONTINUOUS_SNAPPING,
   GADGET_ID_BLOCK_SNAP_FIELD,
   GADGET_ID_BLOCK_LAST_FIELD,
@@ -946,6 +948,7 @@ enum
 enum
 {
   ED_CHECKBUTTON_ID_AUTO_COUNT_GEMS,
+  ED_CHECKBUTTON_ID_RATE_TIME_OVER_SCORE,
   ED_CHECKBUTTON_ID_USE_LEVELSET_ARTWORK,
   ED_CHECKBUTTON_ID_COPY_LEVEL_TEMPLATE,
   ED_CHECKBUTTON_ID_RANDOM_RESTRICTED,
@@ -965,6 +968,7 @@ enum
   ED_CHECKBUTTON_ID_AUTO_EXIT_SOKOBAN,
   ED_CHECKBUTTON_ID_SOLVED_BY_ONE_PLAYER,
   ED_CHECKBUTTON_ID_FINISH_DIG_COLLECT,
+  ED_CHECKBUTTON_ID_KEEP_WALKABLE_CE,
   ED_CHECKBUTTON_ID_CONTINUOUS_SNAPPING,
   ED_CHECKBUTTON_ID_BLOCK_SNAP_FIELD,
   ED_CHECKBUTTON_ID_BLOCK_LAST_FIELD,
@@ -1019,7 +1023,7 @@ enum
 };
 
 #define ED_CHECKBUTTON_ID_LEVEL_FIRST  ED_CHECKBUTTON_ID_AUTO_COUNT_GEMS
-#define ED_CHECKBUTTON_ID_LEVEL_LAST   ED_CHECKBUTTON_ID_AUTO_COUNT_GEMS
+#define ED_CHECKBUTTON_ID_LEVEL_LAST   ED_CHECKBUTTON_ID_RATE_TIME_OVER_SCORE
 
 #define ED_CHECKBUTTON_ID_LEVELSET_FIRST ED_CHECKBUTTON_ID_USE_LEVELSET_ARTWORK
 #define ED_CHECKBUTTON_ID_LEVELSET_LAST         ED_CHECKBUTTON_ID_COPY_LEVEL_TEMPLATE
@@ -1423,7 +1427,7 @@ static struct
     "score for time or steps left:",   NULL, NULL
   },
   {
-    ED_LEVEL_SETTINGS_XPOS(0),         ED_LEVEL_SETTINGS_YPOS(12),
+    ED_LEVEL_SETTINGS_XPOS(0),         ED_LEVEL_SETTINGS_YPOS(13),
     0,                                 9999,
     GADGET_ID_LEVEL_RANDOM_SEED_DOWN,  GADGET_ID_LEVEL_RANDOM_SEED_UP,
     GADGET_ID_LEVEL_RANDOM_SEED_TEXT,  GADGET_ID_NONE,
@@ -2510,7 +2514,7 @@ static struct
     NULL, NULL, NULL,                  "time score for 1 or 10 seconds/steps"
   },
   {
-    ED_LEVEL_SETTINGS_XPOS(0),         ED_LEVEL_SETTINGS_YPOS(11),
+    ED_LEVEL_SETTINGS_XPOS(0),         ED_LEVEL_SETTINGS_YPOS(12),
     GADGET_ID_GAME_ENGINE_TYPE,                GADGET_ID_NONE,
     -1,
     options_game_engine_type,
@@ -3025,6 +3029,13 @@ static struct
     NULL, NULL,
     "automatically count gems needed", "set counter to number of gems"
   },
+  {
+    ED_LEVEL_SETTINGS_XPOS(0),         ED_LEVEL_SETTINGS_YPOS(11),
+    GADGET_ID_RATE_TIME_OVER_SCORE,    GADGET_ID_NONE,
+    &level.rate_time_over_score,
+    NULL, NULL,
+    "rate time/steps used over score", "sort high scores by playing time/steps"
+  },
   {
     ED_LEVEL_SETTINGS_XPOS(0),         ED_LEVEL_SETTINGS_YPOS(7),
     GADGET_ID_USE_LEVELSET_ARTWORK,    GADGET_ID_NONE,
@@ -3161,6 +3172,13 @@ static struct
     NULL, NULL,
     "CE action on finished dig/collect", "only finished dig/collect triggers CE"
   },
+  {
+    ED_ELEMENT_SETTINGS_XPOS(0),       ED_ELEMENT_SETTINGS_YPOS(4),
+    GADGET_ID_KEEP_WALKABLE_CE,                GADGET_ID_NONE,
+    &level.keep_walkable_ce,
+    NULL, NULL,
+    "keep walkable CE changed to player", "keep CE changing to player if walkable"
+  },
   {
     ED_ELEMENT_SETTINGS_XPOS(0),       ED_ELEMENT_SETTINGS_YPOS(9),
     GADGET_ID_CONTINUOUS_SNAPPING,     GADGET_ID_NONE,
@@ -8247,7 +8265,7 @@ static void CopyGroupElementPropertiesToEditor(int element)
 
 static void CopyClassicElementPropertiesToEditor(int element)
 {
-  if (ELEM_IS_PLAYER(element) || COULD_MOVE_INTO_ACID(element))
+  if (IS_PLAYER_ELEMENT(element) || COULD_MOVE_INTO_ACID(element))
     custom_element_properties[EP_CAN_MOVE_INTO_ACID] =
       getMoveIntoAcidProperty(&level, element);
 
@@ -8433,7 +8451,7 @@ static void CopyGroupElementPropertiesToGame(int element)
 
 static void CopyClassicElementPropertiesToGame(int element)
 {
-  if (ELEM_IS_PLAYER(element) || COULD_MOVE_INTO_ACID(element))
+  if (IS_PLAYER_ELEMENT(element) || COULD_MOVE_INTO_ACID(element))
     setMoveIntoAcidProperty(&level, element,
                            custom_element_properties[EP_CAN_MOVE_INTO_ACID]);
 
@@ -9103,7 +9121,7 @@ static void DrawPropertiesTabulatorGadgets(void)
   int i;
 
   // draw two config tabulators for player elements
-  if (ELEM_IS_PLAYER(properties_element))
+  if (IS_PLAYER_ELEMENT(properties_element))
     id_last = ED_TEXTBUTTON_ID_PROPERTIES_CONFIG_2;
 
   // draw two config and one "change" tabulator for custom elements
@@ -9118,7 +9136,7 @@ static void DrawPropertiesTabulatorGadgets(void)
 
     // use "config 1" and "config 2" instead of "config" for players and CEs
     if (i == ED_TEXTBUTTON_ID_PROPERTIES_CONFIG &&
-       (ELEM_IS_PLAYER(properties_element) ||
+       (IS_PLAYER_ELEMENT(properties_element) ||
         IS_CUSTOM_ELEMENT(properties_element)))
       continue;
 
@@ -9815,7 +9833,7 @@ static boolean checkPropertiesConfig(int element)
       IS_ENVELOPE(element) ||
       IS_MM_MCDUFFIN(element) ||
       IS_DF_LASER(element) ||
-      ELEM_IS_PLAYER(element) ||
+      IS_PLAYER_ELEMENT(element) ||
       HAS_EDITOR_CONTENT(element) ||
       CAN_GROW(element) ||
       COULD_MOVE_INTO_ACID(element) ||
@@ -9972,7 +9990,7 @@ static void DrawPropertiesConfig(void)
       DrawAndroidElementArea(properties_element);
   }
 
-  if (ELEM_IS_PLAYER(properties_element))
+  if (IS_PLAYER_ELEMENT(properties_element))
   {
     int player_nr = GET_PLAYER_NR(properties_element);
 
@@ -10035,6 +10053,7 @@ static void DrawPropertiesConfig(void)
       // draw checkbutton gadgets
       MapCheckbuttonGadget(ED_CHECKBUTTON_ID_USE_INITIAL_INVENTORY);
       MapCheckbuttonGadget(ED_CHECKBUTTON_ID_FINISH_DIG_COLLECT);
+      MapCheckbuttonGadget(ED_CHECKBUTTON_ID_KEEP_WALKABLE_CE);
 
       // draw counter gadgets
       MapCounterButtons(ED_COUNTER_ID_INVENTORY_SIZE);
@@ -10051,7 +10070,7 @@ static void DrawPropertiesConfig(void)
     MapCheckbuttonGadget(ED_CHECKBUTTON_ID_EM_EXPLODES_BY_FIRE);
 
   if (COULD_MOVE_INTO_ACID(properties_element) &&
-      !ELEM_IS_PLAYER(properties_element) &&
+      !IS_PLAYER_ELEMENT(properties_element) &&
       (!IS_CUSTOM_ELEMENT(properties_element) ||
        edit_mode_properties == ED_MODE_PROPERTIES_CONFIG_2))
   {
@@ -10389,12 +10408,12 @@ static void DrawPropertiesWindow(void)
     edit_mode_properties = ED_MODE_PROPERTIES_CONFIG_2;
 
   if (edit_mode_properties > ED_MODE_PROPERTIES_CONFIG &&
-      !ELEM_IS_PLAYER(properties_element) &&
+      !IS_PLAYER_ELEMENT(properties_element) &&
       !IS_CUSTOM_ELEMENT(properties_element))
     edit_mode_properties = ED_MODE_PROPERTIES_CONFIG;
 
   if (edit_mode_properties == ED_MODE_PROPERTIES_CONFIG &&
-      (ELEM_IS_PLAYER(properties_element) ||
+      (IS_PLAYER_ELEMENT(properties_element) ||
        IS_CUSTOM_ELEMENT(properties_element)))
     edit_mode_properties = ED_MODE_PROPERTIES_CONFIG_1;
 
@@ -12302,7 +12321,7 @@ static void CopyBrushExt(int from_x, int from_y, int to_x, int to_y,
        {
          int element = Tile[x][y];
 
-         if (!IS_EM_ELEMENT(element) && !ELEM_IS_PLAYER(element))
+         if (!IS_EM_ELEMENT(element) && !IS_PLAYER_ELEMENT(element))
            use_em_engine = FALSE;
 
          if (!IS_SP_ELEMENT(element))
@@ -12985,7 +13004,7 @@ static void HandleDrawingAreas(struct GadgetInfo *gi)
        {
          SetDrawModeHiRes(new_element);
 
-         if (ELEM_IS_PLAYER(new_element))
+         if (IS_PLAYER_ELEMENT(new_element))
          {
            // remove player at old position
            for (y = 0; y < lev_fieldy; y++)
@@ -12994,7 +13013,7 @@ static void HandleDrawingAreas(struct GadgetInfo *gi)
              {
                int old_element = Tile[x][y];
 
-               if (ELEM_IS_PLAYER(old_element))
+               if (IS_PLAYER_ELEMENT(old_element))
                {
                  int replaced_with_element =
                    (old_element == EL_SOKOBAN_FIELD_PLAYER &&
index fe82932..13723bf 100644 (file)
@@ -108,7 +108,7 @@ static int FilterEvents(const Event *event)
     {
       SetMouseCursor(CURSOR_DEFAULT);
 
-      DelayReached(&special_cursor_delay, 0);
+      ResetDelayCounter(&special_cursor_delay);
 
       cursor_mode_last = CURSOR_DEFAULT;
     }
@@ -336,7 +336,7 @@ static void HandleMouseCursor(void)
 
     // display normal pointer if mouse pressed
     if (button_status != MB_RELEASED)
-      DelayReached(&special_cursor_delay, 0);
+      ResetDelayCounter(&special_cursor_delay);
 
     if (gfx.cursor_mode != CURSOR_PLAYFIELD &&
        cursor_inside_playfield &&
@@ -2091,6 +2091,8 @@ void HandleKey(Key key, int key_status)
          {
            key_action      |= key_info[i].action | JOY_BUTTON_SNAP;
            key_snap_action |= key_info[i].action;
+
+           tape.property_bits |= TAPE_PROPERTY_TAS_KEYS;
          }
        }
       }
index 6c26bb0..fa5b9e4 100644 (file)
@@ -61,6 +61,8 @@
 #define TAPE_CHUNK_HEAD_UNUSED 1       // unused tape header bytes
 #define TAPE_CHUNK_SCRN_SIZE   2       // size of screen size chunk
 
+#define SCORE_CHUNK_VERS_SIZE  8       // size of file version chunk
+
 #define LEVEL_CHUNK_CNT3_SIZE(x)        (LEVEL_CHUNK_CNT3_HEADER + (x))
 #define LEVEL_CHUNK_CUS3_SIZE(x)        (2 + (x) * LEVEL_CPART_CUS3_SIZE)
 #define LEVEL_CHUNK_CUS4_SIZE(x)        (96 + (x) * 48)
@@ -68,7 +70,7 @@
 // file identifier strings
 #define LEVEL_COOKIE_TMPL              "ROCKSNDIAMONDS_LEVEL_FILE_VERSION_x.x"
 #define TAPE_COOKIE_TMPL               "ROCKSNDIAMONDS_TAPE_FILE_VERSION_x.x"
-#define SCORE_COOKIE                   "ROCKSNDIAMONDS_SCORE_FILE_VERSION_1.2"
+#define SCORE_COOKIE_TMPL              "ROCKSNDIAMONDS_SCORE_FILE_VERSION_x.x"
 
 // values for deciding when (not) to save configuration data
 #define SAVE_CONF_NEVER                        0
@@ -264,6 +266,12 @@ static struct LevelFileConfigInfo chunk_config_INFO[] =
     &li.time_score_base,               1
   },
 
+  {
+    -1,                                        -1,
+    TYPE_BOOLEAN,                      CONF_VALUE_8_BIT(13),
+    &li.rate_time_over_score,          FALSE
+  },
+
   {
     -1,                                        -1,
     -1,                                        -1,
@@ -319,6 +327,11 @@ static struct LevelFileConfigInfo chunk_config_ELEM[] =
     TYPE_BOOLEAN,                      CONF_VALUE_8_BIT(16),
     &li.finish_dig_collect,            TRUE
   },
+  {
+    EL_PLAYER_1,                       -1,
+    TYPE_BOOLEAN,                      CONF_VALUE_8_BIT(17),
+    &li.keep_walkable_ce,              FALSE
+  },
 
   // (these values are different for each player)
   {
@@ -3147,7 +3160,7 @@ static int LoadLevel_MicroChunk(File *file, struct LevelFileConfigInfo *conf,
          value = getMappedElement(value);
 
        if (data_type == TYPE_BOOLEAN)
-         *(boolean *)(conf[i].value) = value;
+         *(boolean *)(conf[i].value) = (value ? TRUE : FALSE);
        else
          *(int *)    (conf[i].value) = value;
 
@@ -3629,7 +3642,7 @@ static void CopyNativeLevel_RND_to_EM(struct LevelInfo *level)
   // initialize player positions and delete players from the playfield
   for (y = 0; y < cav->height; y++) for (x = 0; x < cav->width; x++)
   {
-    if (ELEM_IS_PLAYER(level->field[x][y]))
+    if (IS_PLAYER_ELEMENT(level->field[x][y]))
     {
       int player_nr = GET_PLAYER_NR(level->field[x][y]);
 
@@ -3930,12 +3943,11 @@ static void CopyNativeLevel_SP_to_RND(struct LevelInfo *level)
   level->time_wheel = 0;
   level->amoeba_content = EL_EMPTY;
 
-#if 1
-  // original Supaplex does not use score values -- use default values
-#else
+  // original Supaplex does not use score values -- rate by playing time
   for (i = 0; i < LEVEL_SCORE_ELEMENTS; i++)
     level->score[i] = 0;
-#endif
+
+  level->rate_time_over_score = TRUE;
 
   // there are no yamyams in supaplex levels
   for (i = 0; i < level->num_yamyam_contents; i++)
@@ -5883,6 +5895,21 @@ int getMappedElement_SB(int element_ascii, boolean use_ces)
   return EL_UNDEFINED;
 }
 
+static void SetLevelSettings_SB(struct LevelInfo *level)
+{
+  // time settings
+  level->time = 0;
+  level->use_step_counter = TRUE;
+
+  // score settings
+  level->score[SC_TIME_BONUS] = 0;
+  level->time_score_base = 1;
+  level->rate_time_over_score = TRUE;
+
+  // game settings
+  level->auto_exit_sokoban = TRUE;
+}
+
 static void LoadLevelFromFileInfo_SB(struct LevelInfo *level,
                                     struct LevelFileInfo *level_file_info,
                                     boolean level_info_only)
@@ -6116,14 +6143,11 @@ static void LoadLevelFromFileInfo_SB(struct LevelInfo *level,
   }
 
   // set special level settings for Sokoban levels
-
-  level->time = 0;
-  level->use_step_counter = TRUE;
+  SetLevelSettings_SB(level);
 
   if (load_xsb_to_ces)
   {
     // special global settings can now be set in level template
-
     level->use_custom_template = TRUE;
   }
 }
@@ -6456,6 +6480,34 @@ static void LoadLevel_InitVersion(struct LevelInfo *level)
   // CE actions were triggered by unfinished digging/collecting up to 4.2.2.0
   if (level->game_version <= VERSION_IDENT(4,2,2,0))
     level->finish_dig_collect = FALSE;
+
+  // CE changing to player was kept under the player if walkable up to 4.2.3.1
+  if (level->game_version <= VERSION_IDENT(4,2,3,1))
+    level->keep_walkable_ce = TRUE;
+}
+
+static void LoadLevel_InitSettings_SB(struct LevelInfo *level)
+{
+  boolean is_sokoban_level = TRUE;    // unless non-Sokoban elements found
+  int x, y;
+
+  // check if this level is (not) a Sokoban level
+  for (y = 0; y < level->fieldy; y++)
+    for (x = 0; x < level->fieldx; x++)
+      if (!IS_SB_ELEMENT(Tile[x][y]))
+       is_sokoban_level = FALSE;
+
+  if (is_sokoban_level)
+  {
+    // set special level settings for Sokoban levels
+    SetLevelSettings_SB(level);
+  }
+}
+
+static void LoadLevel_InitSettings(struct LevelInfo *level)
+{
+  // adjust level settings for (non-native) Sokoban-style levels
+  LoadLevel_InitSettings_SB(level);
 }
 
 static void LoadLevel_InitStandardElements(struct LevelInfo *level)
@@ -6603,6 +6655,27 @@ static void LoadLevel_InitCustomElements(struct LevelInfo *level)
       element_info[element].ignition_delay = 8;
     }
   }
+
+  // set mouse click change events to work for left/middle/right mouse button
+  if (level->game_version < VERSION_IDENT(4,2,3,0))
+  {
+    for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++)
+    {
+      int element = EL_CUSTOM_START + i;
+      struct ElementInfo *ei = &element_info[element];
+
+      for (j = 0; j < ei->num_change_pages; j++)
+      {
+       struct ElementChangeInfo *change = &ei->change_page[j];
+
+       if (change->has_event[CE_CLICKED_BY_MOUSE] ||
+           change->has_event[CE_PRESSED_BY_MOUSE] ||
+           change->has_event[CE_MOUSE_CLICKED_ON_X] ||
+           change->has_event[CE_MOUSE_PRESSED_ON_X])
+         change->trigger_side = CH_SIDE_ANY;
+      }
+    }
+  }
 }
 
 static void LoadLevel_InitElements(struct LevelInfo *level)
@@ -6663,6 +6736,7 @@ static void LoadLevelTemplate_LoadAndInit(void)
 
   LoadLevel_InitVersion(&level_template);
   LoadLevel_InitElements(&level_template);
+  LoadLevel_InitSettings(&level_template);
 
   ActivateLevelTemplate();
 }
@@ -6703,6 +6777,7 @@ static void LoadLevel_LoadAndInit(struct NetworkLevelInfo *network_level)
   LoadLevel_InitVersion(&level);
   LoadLevel_InitElements(&level);
   LoadLevel_InitPlayfield(&level);
+  LoadLevel_InitSettings(&level);
 
   LoadLevel_InitNativeEngines(&level);
 }
@@ -7628,10 +7703,33 @@ void DumpLevel(struct LevelInfo *level)
   Print("SP player blocks last field: %s\n", (level->sp_block_last_field ? "yes" : "no"));
   Print("use spring bug: %s\n", (level->use_spring_bug ? "yes" : "no"));
   Print("use step counter: %s\n", (level->use_step_counter ? "yes" : "no"));
+  Print("rate time over score: %s\n", (level->rate_time_over_score ? "yes" : "no"));
 
   PrintLine("-", 79);
 }
 
+void DumpLevels(void)
+{
+  static LevelDirTree *dumplevel_leveldir = NULL;
+
+  dumplevel_leveldir = getTreeInfoFromIdentifier(leveldir_first,
+                                                global.dumplevel_leveldir);
+
+  if (dumplevel_leveldir == NULL)
+    Fail("no such level identifier: '%s'", global.dumplevel_leveldir);
+
+  if (global.dumplevel_level_nr < dumplevel_leveldir->first_level ||
+      global.dumplevel_level_nr > dumplevel_leveldir->last_level)
+    Fail("no such level number: %d", global.dumplevel_level_nr);
+
+  leveldir_current = dumplevel_leveldir;
+
+  LoadLevel(global.dumplevel_level_nr);
+  DumpLevel(&level);
+
+  CloseAllAndExit(0);
+}
+
 
 // ============================================================================
 // tape file functions
@@ -7665,6 +7763,7 @@ static void setTapeInfoToDefaults(void)
   tape.scr_fieldx = SCR_FIELDX_DEFAULT;
   tape.scr_fieldy = SCR_FIELDY_DEFAULT;
 
+  tape.no_info_chunk = TRUE;
   tape.no_valid_file = FALSE;
 }
 
@@ -7769,6 +7868,8 @@ static int LoadTape_INFO(File *file, int chunk_size, struct TapeInfo *tape)
   int level_identifier_size;
   int i;
 
+  tape->no_info_chunk = FALSE;
+
   level_identifier_size = getFile16BitBE(file);
 
   level_identifier = checked_malloc(level_identifier_size);
@@ -8267,13 +8368,10 @@ void SaveTapeToFilename(char *filename)
   SetFilePermissions(filename, PERMS_PRIVATE);
 }
 
-void SaveTape(int nr)
+static void SaveTapeExt(char *filename)
 {
-  char *filename = getTapeFilename(nr);
   int i;
 
-  InitTapeDirectory(leveldir_current->subdir);
-
   tape.file_version = FILE_VERSION_ACTUAL;
   tape.game_version = GAME_VERSION_ACTUAL;
 
@@ -8289,6 +8387,25 @@ void SaveTape(int nr)
   tape.changed = FALSE;
 }
 
+void SaveTape(int nr)
+{
+  char *filename = getTapeFilename(nr);
+
+  InitTapeDirectory(leveldir_current->subdir);
+
+  SaveTapeExt(filename);
+}
+
+void SaveScoreTape(int nr)
+{
+  char *filename = getScoreTapeFilename(tape.score_tape_basename, nr);
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  InitScoreTapeDirectory(levelset.identifier, nr);
+
+  SaveTapeExt(filename);
+}
+
 static boolean SaveTapeCheckedExt(int nr, char *msg_replace, char *msg_saved,
                                  unsigned int req_state_added)
 {
@@ -8333,11 +8450,43 @@ void DumpTape(struct TapeInfo *tape)
   }
 
   PrintLine("-", 79);
+
   Print("Tape of Level %03d (file version %08d, game version %08d)\n",
        tape->level_nr, tape->file_version, tape->game_version);
   Print("                  (effective engine version %08d)\n",
        tape->engine_version);
   Print("Level series identifier: '%s'\n", tape->level_identifier);
+
+  Print("Special tape properties: ");
+  if (tape->property_bits == TAPE_PROPERTY_NONE)
+    Print("[none]");
+  if (tape->property_bits & TAPE_PROPERTY_EM_RANDOM_BUG)
+    Print("[em_random_bug]");
+  if (tape->property_bits & TAPE_PROPERTY_GAME_SPEED)
+    Print("[game_speed]");
+  if (tape->property_bits & TAPE_PROPERTY_PAUSE_MODE)
+    Print("[pause]");
+  if (tape->property_bits & TAPE_PROPERTY_SINGLE_STEP)
+    Print("[single_step]");
+  if (tape->property_bits & TAPE_PROPERTY_SNAPSHOT)
+    Print("[snapshot]");
+  if (tape->property_bits & TAPE_PROPERTY_REPLAYED)
+    Print("[replayed]");
+  if (tape->property_bits & TAPE_PROPERTY_TAS_KEYS)
+    Print("[tas_keys]");
+  if (tape->property_bits & TAPE_PROPERTY_SMALL_GRAPHICS)
+    Print("[small_graphics]");
+  Print("\n");
+
+  int year2 = tape->date / 10000;
+  int year4 = (year2 < 70 ? 2000 + year2 : 1900 + year2);
+  int month_index_raw = (tape->date / 100) % 100;
+  int month_index = month_index_raw % 12;      // prevent invalid index
+  int month = month_index + 1;
+  int day = tape->date % 100;
+
+  Print("Tape date: %04d-%02d-%02d\n", year4, month, day);
+
   PrintLine("-", 79);
 
   tape_frame_counter = 0;
@@ -8375,12 +8524,69 @@ void DumpTape(struct TapeInfo *tape)
   PrintLine("-", 79);
 }
 
+void DumpTapes(void)
+{
+  static LevelDirTree *dumptape_leveldir = NULL;
+
+  dumptape_leveldir = getTreeInfoFromIdentifier(leveldir_first,
+                                               global.dumptape_leveldir);
+
+  if (dumptape_leveldir == NULL)
+    Fail("no such level identifier: '%s'", global.dumptape_leveldir);
+
+  if (global.dumptape_level_nr < dumptape_leveldir->first_level ||
+      global.dumptape_level_nr > dumptape_leveldir->last_level)
+    Fail("no such level number: %d", global.dumptape_level_nr);
+
+  leveldir_current = dumptape_leveldir;
+
+  if (options.mytapes)
+    LoadTape(global.dumptape_level_nr);
+  else
+    LoadSolutionTape(global.dumptape_level_nr);
+
+  DumpTape(&tape);
+
+  CloseAllAndExit(0);
+}
+
 
 // ============================================================================
 // score file functions
 // ============================================================================
 
-void LoadScore(int nr)
+static void setScoreInfoToDefaultsExt(struct ScoreInfo *scores)
+{
+  int i;
+
+  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+  {
+    strcpy(scores->entry[i].tape_basename, UNDEFINED_FILENAME);
+    strcpy(scores->entry[i].name, EMPTY_PLAYER_NAME);
+    scores->entry[i].score = 0;
+    scores->entry[i].time = 0;
+  }
+
+  scores->num_entries = 0;
+  scores->last_added = -1;
+  scores->last_added_local = -1;
+
+  scores->updated = FALSE;
+  scores->uploaded = FALSE;
+  scores->force_last_added = FALSE;
+}
+
+static void setScoreInfoToDefaults(void)
+{
+  setScoreInfoToDefaultsExt(&scores);
+}
+
+static void setServerScoreInfoToDefaults(void)
+{
+  setScoreInfoToDefaultsExt(&server_scores);
+}
+
+static void LoadScore_OLD(int nr)
 {
   int i;
   char *filename = getScoreFilename(nr);
@@ -8389,13 +8595,6 @@ void LoadScore(int nr)
   char *line_ptr;
   FILE *file;
 
-  // always start with reliable default values
-  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
-  {
-    strcpy(highscore[i].Name, EMPTY_PLAYER_NAME);
-    highscore[i].Score = 0;
-  }
-
   if (!(file = fopen(filename, MODE_READ)))
     return;
 
@@ -8405,7 +8604,7 @@ void LoadScore(int nr)
   if (strlen(cookie) > 0 && cookie[strlen(cookie) - 1] == '\n')
     cookie[strlen(cookie) - 1] = '\0';
 
-  if (!checkCookieString(cookie, SCORE_COOKIE))
+  if (!checkCookieString(cookie, SCORE_COOKIE_TMPL))
   {
     Warn("unknown format of score file '%s'", filename);
 
@@ -8416,7 +8615,7 @@ void LoadScore(int nr)
 
   for (i = 0; i < MAX_SCORE_ENTRIES; i++)
   {
-    if (fscanf(file, "%d", &highscore[i].Score) == EOF)
+    if (fscanf(file, "%d", &scores.entry[i].score) == EOF)
       Warn("fscanf() failed; %s", strerror(errno));
 
     if (fgets(line, MAX_LINE_LEN, file) == NULL)
@@ -8429,8 +8628,8 @@ void LoadScore(int nr)
     {
       if (*line_ptr != ' ' && *line_ptr != '\t' && *line_ptr != '\0')
       {
-       strncpy(highscore[i].Name, line_ptr, MAX_PLAYER_NAME_LEN);
-       highscore[i].Name[MAX_PLAYER_NAME_LEN] = '\0';
+       strncpy(scores.entry[i].name, line_ptr, MAX_PLAYER_NAME_LEN);
+       scores.entry[i].name[MAX_PLAYER_NAME_LEN] = '\0';
        break;
       }
     }
@@ -8439,69 +8638,1191 @@ void LoadScore(int nr)
   fclose(file);
 }
 
-void SaveScore(int nr)
+static void ConvertScore_OLD(void)
 {
+  // only convert score to time for levels that rate playing time over score
+  if (!level.rate_time_over_score)
+    return;
+
+  // convert old score to playing time for score-less levels (like Supaplex)
+  int time_final_max = 999;
   int i;
-  int permissions = (program.global_scores ? PERMS_PUBLIC : PERMS_PRIVATE);
-  char *filename = getScoreFilename(nr);
-  FILE *file;
 
-  // used instead of "leveldir_current->subdir" (for network games)
-  InitScoreDirectory(levelset.identifier);
+  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+  {
+    int score = scores.entry[i].score;
 
-  if (!(file = fopen(filename, MODE_WRITE)))
+    if (score > 0 && score < time_final_max)
+      scores.entry[i].time = (time_final_max - score - 1) * FRAMES_PER_SECOND;
+  }
+}
+
+static int LoadScore_VERS(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+  scores->file_version = getFileVersion(file);
+  scores->game_version = getFileVersion(file);
+
+  return chunk_size;
+}
+
+static int LoadScore_INFO(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+  char *level_identifier = NULL;
+  int level_identifier_size;
+  int i;
+
+  level_identifier_size = getFile16BitBE(file);
+
+  level_identifier = checked_malloc(level_identifier_size);
+
+  for (i = 0; i < level_identifier_size; i++)
+    level_identifier[i] = getFile8Bit(file);
+
+  strncpy(scores->level_identifier, level_identifier, MAX_FILENAME_LEN);
+  scores->level_identifier[MAX_FILENAME_LEN] = '\0';
+
+  checked_free(level_identifier);
+
+  scores->level_nr = getFile16BitBE(file);
+  scores->num_entries = getFile16BitBE(file);
+
+  chunk_size = 2 + level_identifier_size + 2 + 2;
+
+  return chunk_size;
+}
+
+static int LoadScore_NAME(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+  int i, j;
+
+  for (i = 0; i < scores->num_entries; i++)
   {
-    Warn("cannot save score for level %d", nr);
+    for (j = 0; j < MAX_PLAYER_NAME_LEN; j++)
+      scores->entry[i].name[j] = getFile8Bit(file);
 
-    return;
+    scores->entry[i].name[MAX_PLAYER_NAME_LEN] = '\0';
   }
 
-  fprintf(file, "%s\n\n", SCORE_COOKIE);
+  chunk_size = scores->num_entries * MAX_PLAYER_NAME_LEN;
 
-  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
-    fprintf(file, "%d %s\n", highscore[i].Score, highscore[i].Name);
+  return chunk_size;
+}
 
-  fclose(file);
+static int LoadScore_SCOR(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+  int i;
+
+  for (i = 0; i < scores->num_entries; i++)
+    scores->entry[i].score = getFile16BitBE(file);
 
-  SetFilePermissions(filename, permissions);
+  chunk_size = scores->num_entries * 2;
+
+  return chunk_size;
 }
 
+static int LoadScore_TIME(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+  int i;
 
-// ============================================================================
-// setup file functions
-// ============================================================================
+  for (i = 0; i < scores->num_entries; i++)
+    scores->entry[i].time = getFile32BitBE(file);
 
-#define TOKEN_STR_PLAYER_PREFIX                        "player_"
+  chunk_size = scores->num_entries * 4;
 
+  return chunk_size;
+}
 
-static struct TokenInfo global_setup_tokens[] =
+static int LoadScore_TAPE(File *file, int chunk_size, struct ScoreInfo *scores)
 {
+  int i, j;
+
+  for (i = 0; i < scores->num_entries; i++)
   {
-    TYPE_STRING,
-    &setup.player_name,                                "player_name"
-  },
-  {
-    TYPE_SWITCH,
-    &setup.multiple_users,                     "multiple_users"
-  },
-  {
-    TYPE_SWITCH,
-    &setup.sound,                              "sound"
-  },
+    for (j = 0; j < MAX_SCORE_TAPE_BASENAME_LEN; j++)
+      scores->entry[i].tape_basename[j] = getFile8Bit(file);
+
+    scores->entry[i].tape_basename[MAX_SCORE_TAPE_BASENAME_LEN] = '\0';
+  }
+
+  chunk_size = scores->num_entries * MAX_SCORE_TAPE_BASENAME_LEN;
+
+  return chunk_size;
+}
+
+void LoadScore(int nr)
+{
+  char *filename = getScoreFilename(nr);
+  char cookie[MAX_LINE_LEN];
+  char chunk_name[CHUNK_ID_LEN + 1];
+  int chunk_size;
+  boolean old_score_file_format = FALSE;
+  File *file;
+
+  // always start with reliable default values
+  setScoreInfoToDefaults();
+
+  if (!(file = openFile(filename, MODE_READ)))
+    return;
+
+  getFileChunkBE(file, chunk_name, NULL);
+  if (strEqual(chunk_name, "RND1"))
   {
-    TYPE_SWITCH,
-    &setup.sound_loops,                                "repeating_sound_loops"
-  },
+    getFile32BitBE(file);              // not used
+
+    getFileChunkBE(file, chunk_name, NULL);
+    if (!strEqual(chunk_name, "SCOR"))
+    {
+      Warn("unknown format of score file '%s'", filename);
+
+      closeFile(file);
+
+      return;
+    }
+  }
+  else // check for old file format with cookie string
   {
-    TYPE_SWITCH,
-    &setup.sound_music,                                "background_music"
-  },
+    strcpy(cookie, chunk_name);
+    if (getStringFromFile(file, &cookie[4], MAX_LINE_LEN - 4) == NULL)
+      cookie[4] = '\0';
+    if (strlen(cookie) > 0 && cookie[strlen(cookie) - 1] == '\n')
+      cookie[strlen(cookie) - 1] = '\0';
+
+    if (!checkCookieString(cookie, SCORE_COOKIE_TMPL))
+    {
+      Warn("unknown format of score file '%s'", filename);
+
+      closeFile(file);
+
+      return;
+    }
+
+    old_score_file_format = TRUE;
+  }
+
+  if (old_score_file_format)
   {
-    TYPE_SWITCH,
-    &setup.sound_simple,                       "simple_sound_effects"
-  },
+    // score files from versions before 4.2.4.0 without chunk structure
+    LoadScore_OLD(nr);
+
+    // convert score to time, if possible (mainly for Supaplex levels)
+    ConvertScore_OLD();
+  }
+  else
   {
-    TYPE_SWITCH,
+    static struct
+    {
+      char *name;
+      int size;
+      int (*loader)(File *, int, struct ScoreInfo *);
+    }
+    chunk_info[] =
+    {
+      { "VERS", SCORE_CHUNK_VERS_SIZE, LoadScore_VERS },
+      { "INFO", -1,                    LoadScore_INFO },
+      { "NAME", -1,                    LoadScore_NAME },
+      { "SCOR", -1,                    LoadScore_SCOR },
+      { "TIME", -1,                    LoadScore_TIME },
+      { "TAPE", -1,                    LoadScore_TAPE },
+
+      {  NULL,  0,                     NULL }
+    };
+
+    while (getFileChunkBE(file, chunk_name, &chunk_size))
+    {
+      int i = 0;
+
+      while (chunk_info[i].name != NULL &&
+            !strEqual(chunk_name, chunk_info[i].name))
+       i++;
+
+      if (chunk_info[i].name == NULL)
+      {
+       Warn("unknown chunk '%s' in score file '%s'",
+             chunk_name, filename);
+
+       ReadUnusedBytesFromFile(file, chunk_size);
+      }
+      else if (chunk_info[i].size != -1 &&
+              chunk_info[i].size != chunk_size)
+      {
+       Warn("wrong size (%d) of chunk '%s' in score file '%s'",
+             chunk_size, chunk_name, filename);
+
+       ReadUnusedBytesFromFile(file, chunk_size);
+      }
+      else
+      {
+       // call function to load this score chunk
+       int chunk_size_expected =
+         (chunk_info[i].loader)(file, chunk_size, &scores);
+
+       // the size of some chunks cannot be checked before reading other
+       // chunks first (like "HEAD" and "BODY") that contain some header
+       // information, so check them here
+       if (chunk_size_expected != chunk_size)
+       {
+         Warn("wrong size (%d) of chunk '%s' in score file '%s'",
+               chunk_size, chunk_name, filename);
+       }
+      }
+    }
+  }
+
+  closeFile(file);
+}
+
+#if ENABLE_HISTORIC_CHUNKS
+void SaveScore_OLD(int nr)
+{
+  int i;
+  char *filename = getScoreFilename(nr);
+  FILE *file;
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  InitScoreDirectory(levelset.identifier);
+
+  if (!(file = fopen(filename, MODE_WRITE)))
+  {
+    Warn("cannot save score for level %d", nr);
+
+    return;
+  }
+
+  fprintf(file, "%s\n\n", SCORE_COOKIE);
+
+  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+    fprintf(file, "%d %s\n", scores.entry[i].score, scores.entry[i].name);
+
+  fclose(file);
+
+  SetFilePermissions(filename, PERMS_PRIVATE);
+}
+#endif
+
+static void SaveScore_VERS(FILE *file, struct ScoreInfo *scores)
+{
+  putFileVersion(file, scores->file_version);
+  putFileVersion(file, scores->game_version);
+}
+
+static void SaveScore_INFO(FILE *file, struct ScoreInfo *scores)
+{
+  int level_identifier_size = strlen(scores->level_identifier) + 1;
+  int i;
+
+  putFile16BitBE(file, level_identifier_size);
+
+  for (i = 0; i < level_identifier_size; i++)
+    putFile8Bit(file, scores->level_identifier[i]);
+
+  putFile16BitBE(file, scores->level_nr);
+  putFile16BitBE(file, scores->num_entries);
+}
+
+static void SaveScore_NAME(FILE *file, struct ScoreInfo *scores)
+{
+  int i, j;
+
+  for (i = 0; i < scores->num_entries; i++)
+  {
+    int name_size = strlen(scores->entry[i].name);
+
+    for (j = 0; j < MAX_PLAYER_NAME_LEN; j++)
+      putFile8Bit(file, (j < name_size ? scores->entry[i].name[j] : 0));
+  }
+}
+
+static void SaveScore_SCOR(FILE *file, struct ScoreInfo *scores)
+{
+  int i;
+
+  for (i = 0; i < scores->num_entries; i++)
+    putFile16BitBE(file, scores->entry[i].score);
+}
+
+static void SaveScore_TIME(FILE *file, struct ScoreInfo *scores)
+{
+  int i;
+
+  for (i = 0; i < scores->num_entries; i++)
+    putFile32BitBE(file, scores->entry[i].time);
+}
+
+static void SaveScore_TAPE(FILE *file, struct ScoreInfo *scores)
+{
+  int i, j;
+
+  for (i = 0; i < scores->num_entries; i++)
+  {
+    int size = strlen(scores->entry[i].tape_basename);
+
+    for (j = 0; j < MAX_SCORE_TAPE_BASENAME_LEN; j++)
+      putFile8Bit(file, (j < size ? scores->entry[i].tape_basename[j] : 0));
+  }
+}
+
+static void SaveScoreToFilename(char *filename)
+{
+  FILE *file;
+  int info_chunk_size;
+  int name_chunk_size;
+  int scor_chunk_size;
+  int time_chunk_size;
+  int tape_chunk_size;
+
+  if (!(file = fopen(filename, MODE_WRITE)))
+  {
+    Warn("cannot save score file '%s'", filename);
+
+    return;
+  }
+
+  info_chunk_size = 2 + (strlen(scores.level_identifier) + 1) + 2 + 2;
+  name_chunk_size = scores.num_entries * MAX_PLAYER_NAME_LEN;
+  scor_chunk_size = scores.num_entries * 2;
+  time_chunk_size = scores.num_entries * 4;
+  tape_chunk_size = scores.num_entries * MAX_SCORE_TAPE_BASENAME_LEN;
+
+  putFileChunkBE(file, "RND1", CHUNK_SIZE_UNDEFINED);
+  putFileChunkBE(file, "SCOR", CHUNK_SIZE_NONE);
+
+  putFileChunkBE(file, "VERS", SCORE_CHUNK_VERS_SIZE);
+  SaveScore_VERS(file, &scores);
+
+  putFileChunkBE(file, "INFO", info_chunk_size);
+  SaveScore_INFO(file, &scores);
+
+  putFileChunkBE(file, "NAME", name_chunk_size);
+  SaveScore_NAME(file, &scores);
+
+  putFileChunkBE(file, "SCOR", scor_chunk_size);
+  SaveScore_SCOR(file, &scores);
+
+  putFileChunkBE(file, "TIME", time_chunk_size);
+  SaveScore_TIME(file, &scores);
+
+  putFileChunkBE(file, "TAPE", tape_chunk_size);
+  SaveScore_TAPE(file, &scores);
+
+  fclose(file);
+
+  SetFilePermissions(filename, PERMS_PRIVATE);
+}
+
+void SaveScore(int nr)
+{
+  char *filename = getScoreFilename(nr);
+  int i;
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  InitScoreDirectory(levelset.identifier);
+
+  scores.file_version = FILE_VERSION_ACTUAL;
+  scores.game_version = GAME_VERSION_ACTUAL;
+
+  strncpy(scores.level_identifier, levelset.identifier, MAX_FILENAME_LEN);
+  scores.level_identifier[MAX_FILENAME_LEN] = '\0';
+  scores.level_nr = level_nr;
+
+  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+    if (scores.entry[i].score == 0 &&
+        scores.entry[i].time == 0 &&
+        strEqual(scores.entry[i].name, EMPTY_PLAYER_NAME))
+      break;
+
+  scores.num_entries = i;
+
+  if (scores.num_entries == 0)
+    return;
+
+  SaveScoreToFilename(filename);
+}
+
+void ExecuteAsThread(SDL_ThreadFunction function, char *name, void *data,
+                    char *error)
+{
+#if defined(PLATFORM_EMSCRIPTEN)
+  // threads currently not fully supported by Emscripten/SDL and some browsers
+  function(data);
+#else
+  SDL_Thread *thread = SDL_CreateThread(function, name, data);
+
+  if (thread != NULL)
+    SDL_DetachThread(thread);
+  else
+    Error("Cannot create thread to %s!", error);
+
+  // nasty kludge to lower probability of intermingled thread error messages
+  Delay(1);
+#endif
+}
+
+char *getPasswordJSON(char *password)
+{
+  static char password_json[MAX_FILENAME_LEN] = "";
+  static boolean initialized = FALSE;
+
+  if (!initialized)
+  {
+    if (password != NULL &&
+       !strEqual(password, "") &&
+       !strEqual(password, UNDEFINED_PASSWORD))
+      snprintf(password_json, MAX_FILENAME_LEN,
+              "  \"password\":             \"%s\",\n",
+              setup.api_server_password);
+
+    initialized = TRUE;
+  }
+
+  return password_json;
+}
+
+struct ApiGetScoreThreadData
+{
+  int level_nr;
+  char *score_cache_filename;
+};
+
+static void *CreateThreadData_ApiGetScore(int nr)
+{
+  struct ApiGetScoreThreadData *data =
+    checked_malloc(sizeof(struct ApiGetScoreThreadData));
+  char *score_cache_filename = getScoreCacheFilename(nr);
+
+  data->level_nr = nr;
+  data->score_cache_filename = getStringCopy(score_cache_filename);
+
+  return data;
+}
+
+static void FreeThreadData_ApiGetScore(void *data_raw)
+{
+  struct ApiGetScoreThreadData *data = data_raw;
+
+  checked_free(data->score_cache_filename);
+  checked_free(data);
+}
+
+static boolean SetRequest_ApiGetScore(struct HttpRequest *request,
+                                     void *data_raw)
+{
+  struct ApiGetScoreThreadData *data = data_raw;
+  int level_nr = data->level_nr;
+
+  request->hostname = setup.api_server_hostname;
+  request->port     = API_SERVER_PORT;
+  request->method   = API_SERVER_METHOD;
+  request->uri      = API_SERVER_URI_GET;
+
+  char *levelset_identifier = getEscapedJSON(leveldir_current->identifier);
+  char *levelset_name       = getEscapedJSON(leveldir_current->name);
+
+  snprintf(request->body, MAX_HTTP_BODY_SIZE,
+          "{\n"
+          "%s"
+          "  \"game_version\":         \"%s\",\n"
+          "  \"game_platform\":        \"%s\",\n"
+          "  \"levelset_identifier\":  \"%s\",\n"
+          "  \"levelset_name\":        \"%s\",\n"
+          "  \"level_nr\":             \"%d\"\n"
+          "}\n",
+          getPasswordJSON(setup.api_server_password),
+          getProgramRealVersionString(),
+          getProgramPlatformString(),
+          levelset_identifier,
+          levelset_name,
+          level_nr);
+
+  checked_free(levelset_identifier);
+  checked_free(levelset_name);
+
+  ConvertHttpRequestBodyToServerEncoding(request);
+
+  return TRUE;
+}
+
+static void HandleResponse_ApiGetScore(struct HttpResponse *response,
+                                      void *data_raw)
+{
+  struct ApiGetScoreThreadData *data = data_raw;
+
+  if (response->body_size == 0)
+  {
+    // no scores available for this level
+
+    return;
+  }
+
+  ConvertHttpResponseBodyToClientEncoding(response);
+
+  char *filename = data->score_cache_filename;
+  FILE *file;
+  int i;
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  InitScoreCacheDirectory(levelset.identifier);
+
+  if (!(file = fopen(filename, MODE_WRITE)))
+  {
+    Warn("cannot save score cache file '%s'", filename);
+
+    return;
+  }
+
+  for (i = 0; i < response->body_size; i++)
+    fputc(response->body[i], file);
+
+  fclose(file);
+
+  SetFilePermissions(filename, PERMS_PRIVATE);
+
+  server_scores.updated = TRUE;
+}
+
+#if defined(PLATFORM_EMSCRIPTEN)
+static void Emscripten_ApiGetScore_Loaded(unsigned handle, void *data_raw,
+                                         void *buffer, unsigned int size)
+{
+  struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
+
+  if (response != NULL)
+  {
+    HandleResponse_ApiGetScore(response, data_raw);
+
+    checked_free(response);
+  }
+  else
+  {
+    Error("server response too large to handle (%d bytes)", size);
+  }
+
+  FreeThreadData_ApiGetScore(data_raw);
+}
+
+static void Emscripten_ApiGetScore_Failed(unsigned handle, void *data_raw,
+                                         int code, const char *status)
+{
+  Error("server failed to handle request: %d %s", code, status);
+
+  FreeThreadData_ApiGetScore(data_raw);
+}
+
+static void Emscripten_ApiGetScore_Progress(unsigned handle, void *data_raw,
+                                           int bytes, int size)
+{
+  // nothing to do here
+}
+
+static void Emscripten_ApiGetScore_HttpRequest(struct HttpRequest *request,
+                                              void *data_raw)
+{
+  if (!SetRequest_ApiGetScore(request, data_raw))
+  {
+    FreeThreadData_ApiGetScore(data_raw);
+
+    return;
+  }
+
+  emscripten_async_wget2_data(request->uri,
+                             request->method,
+                             request->body,
+                             data_raw,
+                             TRUE,
+                             Emscripten_ApiGetScore_Loaded,
+                             Emscripten_ApiGetScore_Failed,
+                             Emscripten_ApiGetScore_Progress);
+}
+
+#else
+
+static void ApiGetScore_HttpRequestExt(struct HttpRequest *request,
+                                      struct HttpResponse *response,
+                                      void *data_raw)
+{
+  if (!SetRequest_ApiGetScore(request, data_raw))
+    return;
+
+  if (!DoHttpRequest(request, response))
+  {
+    Error("HTTP request failed: %s", GetHttpError());
+
+    return;
+  }
+
+  if (!HTTP_SUCCESS(response->status_code))
+  {
+    // do not show error message if no scores found for this level set
+    if (response->status_code == 404)
+      return;
+
+    Error("server failed to handle request: %d %s",
+         response->status_code,
+         response->status_text);
+
+    return;
+  }
+
+  HandleResponse_ApiGetScore(response, data_raw);
+}
+
+static void ApiGetScore_HttpRequest(struct HttpRequest *request,
+                                   struct HttpResponse *response,
+                                   void *data_raw)
+{
+  ApiGetScore_HttpRequestExt(request, response, data_raw);
+
+  FreeThreadData_ApiGetScore(data_raw);
+}
+#endif
+
+static int ApiGetScoreThread(void *data_raw)
+{
+  struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
+  struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
+
+  program.api_thread_count++;
+
+#if defined(PLATFORM_EMSCRIPTEN)
+  Emscripten_ApiGetScore_HttpRequest(request, data_raw);
+#else
+  ApiGetScore_HttpRequest(request, response, data_raw);
+#endif
+
+  program.api_thread_count--;
+
+  checked_free(request);
+  checked_free(response);
+
+  return 0;
+}
+
+static void ApiGetScoreAsThread(int nr)
+{
+  struct ApiGetScoreThreadData *data = CreateThreadData_ApiGetScore(nr);
+
+  ExecuteAsThread(ApiGetScoreThread,
+                 "ApiGetScore", data,
+                 "download scores from server");
+}
+
+static void LoadServerScoreFromCache(int nr)
+{
+  struct ScoreEntry score_entry;
+  struct
+  {
+    void *value;
+    boolean is_string;
+    int string_size;
+  }
+  score_mapping[] =
+  {
+    { &score_entry.score,              FALSE,  0                       },
+    { &score_entry.time,               FALSE,  0                       },
+    { score_entry.name,                        TRUE,   MAX_PLAYER_NAME_LEN     },
+    { score_entry.tape_basename,       TRUE,   MAX_FILENAME_LEN        },
+
+    { NULL,                            FALSE,  0                       }
+  };
+  char *filename = getScoreCacheFilename(nr);
+  SetupFileHash *score_hash = loadSetupFileHash(filename);
+  int i, j;
+
+  server_scores.num_entries = 0;
+
+  if (score_hash == NULL)
+    return;
+
+  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+  {
+    score_entry = server_scores.entry[i];
+
+    for (j = 0; score_mapping[j].value != NULL; j++)
+    {
+      char token[10];
+
+      sprintf(token, "%02d.%d", i, j);
+
+      char *value = getHashEntry(score_hash, token);
+
+      if (value == NULL)
+       continue;
+
+      if (score_mapping[j].is_string)
+      {
+       char *score_value = (char *)score_mapping[j].value;
+       int value_size = score_mapping[j].string_size;
+
+       strncpy(score_value, value, value_size);
+       score_value[value_size] = '\0';
+      }
+      else
+      {
+       int *score_value = (int *)score_mapping[j].value;
+
+       *score_value = atoi(value);
+      }
+
+      server_scores.num_entries = i + 1;
+    }
+
+    server_scores.entry[i] = score_entry;
+  }
+
+  freeSetupFileHash(score_hash);
+}
+
+void LoadServerScore(int nr, boolean download_score)
+{
+  if (!setup.use_api_server)
+    return;
+
+  // always start with reliable default values
+  setServerScoreInfoToDefaults();
+
+  // 1st step: load server scores from cache file (which may not exist)
+  // (this should prevent reading it while the thread is writing to it)
+  LoadServerScoreFromCache(nr);
+
+  if (download_score && runtime.use_api_server)
+  {
+    // 2nd step: download server scores from score server to cache file
+    // (as thread, as it might time out if the server is not reachable)
+    ApiGetScoreAsThread(nr);
+  }
+}
+
+static char *get_file_base64(char *filename)
+{
+  struct stat file_status;
+
+  if (stat(filename, &file_status) != 0)
+  {
+    Error("cannot stat file '%s'", filename);
+
+    return NULL;
+  }
+
+  int buffer_size = file_status.st_size;
+  byte *buffer = checked_malloc(buffer_size);
+  FILE *file;
+  int i;
+
+  if (!(file = fopen(filename, MODE_READ)))
+  {
+    Error("cannot open file '%s'", filename);
+
+    checked_free(buffer);
+
+    return NULL;
+  }
+
+  for (i = 0; i < buffer_size; i++)
+  {
+    int c = fgetc(file);
+
+    if (c == EOF)
+    {
+      Error("cannot read from input file '%s'", filename);
+
+      fclose(file);
+      checked_free(buffer);
+
+      return NULL;
+    }
+
+    buffer[i] = (byte)c;
+  }
+
+  fclose(file);
+
+  int buffer_encoded_size = base64_encoded_size(buffer_size);
+  char *buffer_encoded = checked_malloc(buffer_encoded_size);
+
+  base64_encode(buffer_encoded, buffer, buffer_size);
+
+  checked_free(buffer);
+
+  return buffer_encoded;
+}
+
+static void PrepareScoreTapesForUpload(char *leveldir_subdir)
+{
+  MarkTapeDirectoryUploadsAsIncomplete(leveldir_subdir);
+
+  // if score tape not uploaded, ask for uploading missing tapes later
+  if (!setup.has_remaining_tapes)
+    setup.ask_for_remaining_tapes = TRUE;
+
+  setup.provide_uploading_tapes = TRUE;
+  setup.has_remaining_tapes = TRUE;
+
+  SaveSetup_ServerSetup();
+}
+
+struct ApiAddScoreThreadData
+{
+  int level_nr;
+  boolean tape_saved;
+  char *leveldir_subdir;
+  char *score_tape_filename;
+  struct ScoreEntry score_entry;
+};
+
+static void *CreateThreadData_ApiAddScore(int nr, boolean tape_saved,
+                                         char *score_tape_filename)
+{
+  struct ApiAddScoreThreadData *data =
+    checked_malloc(sizeof(struct ApiAddScoreThreadData));
+  struct ScoreEntry *score_entry = &scores.entry[scores.last_added];
+
+  if (score_tape_filename == NULL)
+    score_tape_filename = getScoreTapeFilename(score_entry->tape_basename, nr);
+
+  data->level_nr = nr;
+  data->tape_saved = tape_saved;
+  data->leveldir_subdir = getStringCopy(leveldir_current->subdir);
+  data->score_tape_filename = getStringCopy(score_tape_filename);
+  data->score_entry = *score_entry;
+
+  return data;
+}
+
+static void FreeThreadData_ApiAddScore(void *data_raw)
+{
+  struct ApiAddScoreThreadData *data = data_raw;
+
+  checked_free(data->leveldir_subdir);
+  checked_free(data->score_tape_filename);
+  checked_free(data);
+}
+
+static boolean SetRequest_ApiAddScore(struct HttpRequest *request,
+                                     void *data_raw)
+{
+  struct ApiAddScoreThreadData *data = data_raw;
+  struct ScoreEntry *score_entry = &data->score_entry;
+  char *score_tape_filename = data->score_tape_filename;
+  boolean tape_saved = data->tape_saved;
+  int level_nr = data->level_nr;
+
+  request->hostname = setup.api_server_hostname;
+  request->port     = API_SERVER_PORT;
+  request->method   = API_SERVER_METHOD;
+  request->uri      = API_SERVER_URI_ADD;
+
+  char *tape_base64 = get_file_base64(score_tape_filename);
+
+  if (tape_base64 == NULL)
+  {
+    Error("loading and base64 encoding score tape file failed");
+
+    return FALSE;
+  }
+
+  char *player_name_raw = score_entry->name;
+  char *player_uuid_raw = setup.player_uuid;
+
+  if (options.player_name != NULL && global.autoplay_leveldir != NULL)
+  {
+    player_name_raw = options.player_name;
+    player_uuid_raw = "";
+  }
+
+  char *levelset_identifier = getEscapedJSON(leveldir_current->identifier);
+  char *levelset_name       = getEscapedJSON(leveldir_current->name);
+  char *levelset_author     = getEscapedJSON(leveldir_current->author);
+  char *level_name          = getEscapedJSON(level.name);
+  char *level_author        = getEscapedJSON(level.author);
+  char *player_name         = getEscapedJSON(player_name_raw);
+  char *player_uuid         = getEscapedJSON(player_uuid_raw);
+
+  snprintf(request->body, MAX_HTTP_BODY_SIZE,
+          "{\n"
+          "%s"
+          "  \"game_version\":         \"%s\",\n"
+          "  \"game_platform\":        \"%s\",\n"
+          "  \"batch_time\":           \"%d\",\n"
+          "  \"levelset_identifier\":  \"%s\",\n"
+          "  \"levelset_name\":        \"%s\",\n"
+          "  \"levelset_author\":      \"%s\",\n"
+          "  \"levelset_num_levels\":  \"%d\",\n"
+          "  \"levelset_first_level\": \"%d\",\n"
+          "  \"level_nr\":             \"%d\",\n"
+          "  \"level_name\":           \"%s\",\n"
+          "  \"level_author\":         \"%s\",\n"
+          "  \"use_step_counter\":     \"%d\",\n"
+          "  \"rate_time_over_score\": \"%d\",\n"
+          "  \"player_name\":          \"%s\",\n"
+          "  \"player_uuid\":          \"%s\",\n"
+          "  \"score\":                \"%d\",\n"
+          "  \"time\":                 \"%d\",\n"
+          "  \"tape_basename\":        \"%s\",\n"
+          "  \"tape_saved\":           \"%d\",\n"
+          "  \"tape\":                 \"%s\"\n"
+          "}\n",
+          getPasswordJSON(setup.api_server_password),
+          getProgramRealVersionString(),
+          getProgramPlatformString(),
+          (int)global.autoplay_time,
+          levelset_identifier,
+          levelset_name,
+          levelset_author,
+          leveldir_current->levels,
+          leveldir_current->first_level,
+          level_nr,
+          level_name,
+          level_author,
+          level.use_step_counter,
+          level.rate_time_over_score,
+          player_name,
+          player_uuid,
+          score_entry->score,
+          score_entry->time,
+          score_entry->tape_basename,
+          tape_saved,
+          tape_base64);
+
+  checked_free(tape_base64);
+
+  checked_free(levelset_identifier);
+  checked_free(levelset_name);
+  checked_free(levelset_author);
+  checked_free(level_name);
+  checked_free(level_author);
+  checked_free(player_name);
+  checked_free(player_uuid);
+
+  ConvertHttpRequestBodyToServerEncoding(request);
+
+  return TRUE;
+}
+
+static void HandleResponse_ApiAddScore(struct HttpResponse *response,
+                                      void *data_raw)
+{
+  server_scores.uploaded = TRUE;
+}
+
+static void HandleFailure_ApiAddScore(void *data_raw)
+{
+  struct ApiAddScoreThreadData *data = data_raw;
+
+  PrepareScoreTapesForUpload(data->leveldir_subdir);
+}
+
+#if defined(PLATFORM_EMSCRIPTEN)
+static void Emscripten_ApiAddScore_Loaded(unsigned handle, void *data_raw,
+                                         void *buffer, unsigned int size)
+{
+  struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
+
+  if (response != NULL)
+  {
+    HandleResponse_ApiAddScore(response, data_raw);
+
+    checked_free(response);
+  }
+  else
+  {
+    Error("server response too large to handle (%d bytes)", size);
+
+    HandleFailure_ApiAddScore(data_raw);
+  }
+
+  FreeThreadData_ApiAddScore(data_raw);
+}
+
+static void Emscripten_ApiAddScore_Failed(unsigned handle, void *data_raw,
+                                         int code, const char *status)
+{
+  Error("server failed to handle request: %d %s", code, status);
+
+  HandleFailure_ApiAddScore(data_raw);
+
+  FreeThreadData_ApiAddScore(data_raw);
+}
+
+static void Emscripten_ApiAddScore_Progress(unsigned handle, void *data_raw,
+                                           int bytes, int size)
+{
+  // nothing to do here
+}
+
+static void Emscripten_ApiAddScore_HttpRequest(struct HttpRequest *request,
+                                              void *data_raw)
+{
+  if (!SetRequest_ApiAddScore(request, data_raw))
+  {
+    FreeThreadData_ApiAddScore(data_raw);
+
+    return;
+  }
+
+  emscripten_async_wget2_data(request->uri,
+                             request->method,
+                             request->body,
+                             data_raw,
+                             TRUE,
+                             Emscripten_ApiAddScore_Loaded,
+                             Emscripten_ApiAddScore_Failed,
+                             Emscripten_ApiAddScore_Progress);
+}
+
+#else
+
+static void ApiAddScore_HttpRequestExt(struct HttpRequest *request,
+                                      struct HttpResponse *response,
+                                      void *data_raw)
+{
+  if (!SetRequest_ApiAddScore(request, data_raw))
+    return;
+
+  if (!DoHttpRequest(request, response))
+  {
+    Error("HTTP request failed: %s", GetHttpError());
+
+    HandleFailure_ApiAddScore(data_raw);
+
+    return;
+  }
+
+  if (!HTTP_SUCCESS(response->status_code))
+  {
+    Error("server failed to handle request: %d %s",
+         response->status_code,
+         response->status_text);
+
+    HandleFailure_ApiAddScore(data_raw);
+
+    return;
+  }
+
+  HandleResponse_ApiAddScore(response, data_raw);
+}
+
+static void ApiAddScore_HttpRequest(struct HttpRequest *request,
+                                   struct HttpResponse *response,
+                                   void *data_raw)
+{
+  ApiAddScore_HttpRequestExt(request, response, data_raw);
+
+  FreeThreadData_ApiAddScore(data_raw);
+}
+#endif
+
+static int ApiAddScoreThread(void *data_raw)
+{
+  struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
+  struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
+
+  program.api_thread_count++;
+
+#if defined(PLATFORM_EMSCRIPTEN)
+  Emscripten_ApiAddScore_HttpRequest(request, data_raw);
+#else
+  ApiAddScore_HttpRequest(request, response, data_raw);
+#endif
+
+  program.api_thread_count--;
+
+  checked_free(request);
+  checked_free(response);
+
+  return 0;
+}
+
+static void ApiAddScoreAsThread(int nr, boolean tape_saved,
+                               char *score_tape_filename)
+{
+  struct ApiAddScoreThreadData *data =
+    CreateThreadData_ApiAddScore(nr, tape_saved, score_tape_filename);
+
+  ExecuteAsThread(ApiAddScoreThread,
+                 "ApiAddScore", data,
+                 "upload score to server");
+}
+
+void SaveServerScore(int nr, boolean tape_saved)
+{
+  if (!runtime.use_api_server)
+  {
+    PrepareScoreTapesForUpload(leveldir_current->subdir);
+
+    return;
+  }
+
+  ApiAddScoreAsThread(nr, tape_saved, NULL);
+}
+
+void SaveServerScoreFromFile(int nr, boolean tape_saved,
+                            char *score_tape_filename)
+{
+  if (!runtime.use_api_server)
+    return;
+
+  ApiAddScoreAsThread(nr, tape_saved, score_tape_filename);
+}
+
+void LoadLocalAndServerScore(int nr, boolean download_score)
+{
+  int last_added_local = scores.last_added_local;
+
+  // needed if only showing server scores
+  setScoreInfoToDefaults();
+
+  if (!strEqual(setup.scores_in_highscore_list, STR_SCORES_TYPE_SERVER_ONLY))
+    LoadScore(nr);
+
+  // restore last added local score entry (before merging server scores)
+  scores.last_added = scores.last_added_local = last_added_local;
+
+  if (setup.use_api_server &&
+      !strEqual(setup.scores_in_highscore_list, STR_SCORES_TYPE_LOCAL_ONLY))
+  {
+    // load server scores from cache file and trigger update from server
+    LoadServerScore(nr, download_score);
+
+    // merge local scores with scores from server
+    MergeServerScore();
+  }
+}
+
+
+// ============================================================================
+// setup file functions
+// ============================================================================
+
+#define TOKEN_STR_PLAYER_PREFIX                        "player_"
+
+
+static struct TokenInfo global_setup_tokens[] =
+{
+  {
+    TYPE_STRING,
+    &setup.player_name,                                "player_name"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.multiple_users,                     "multiple_users"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.sound,                              "sound"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.sound_loops,                                "repeating_sound_loops"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.sound_music,                                "background_music"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.sound_simple,                       "simple_sound_effects"
+  },
+  {
+    TYPE_SWITCH,
     &setup.toons,                              "toons"
   },
   {
@@ -8650,7 +9971,15 @@ static struct TokenInfo global_setup_tokens[] =
   },
   {
     TYPE_SWITCH,
-    &setup.show_snapshot_buttons,              "show_snapshot_buttons"
+    &setup.show_load_save_buttons,             "show_load_save_buttons"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.show_undo_redo_buttons,             "show_undo_redo_buttons"
+  },
+  {
+    TYPE_STRING,
+    &setup.scores_in_highscore_list,           "scores_in_highscore_list"
   },
   {
     TYPE_STRING,
@@ -8750,6 +10079,46 @@ static struct TokenInfo auto_setup_tokens[] =
   },
 };
 
+static struct TokenInfo server_setup_tokens[] =
+{
+  {
+    TYPE_STRING,
+    &setup.player_uuid,                                "player_uuid"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.use_api_server,          TEST_PREFIX        "use_api_server"
+  },
+  {
+    TYPE_STRING,
+    &setup.api_server_hostname,     TEST_PREFIX        "api_server_hostname"
+  },
+  {
+    TYPE_STRING,
+    &setup.api_server_password,     TEST_PREFIX        "api_server_password"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.ask_for_uploading_tapes, TEST_PREFIX        "ask_for_uploading_tapes"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.ask_for_remaining_tapes, TEST_PREFIX        "ask_for_remaining_tapes"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.provide_uploading_tapes, TEST_PREFIX        "provide_uploading_tapes"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.ask_for_using_api_server,TEST_PREFIX        "ask_for_using_api_server"
+  },
+  {
+    TYPE_SWITCH,
+    &setup.has_remaining_tapes,     TEST_PREFIX        "has_remaining_tapes"
+  },
+};
+
 static struct TokenInfo editor_setup_tokens[] =
 {
   {
@@ -9315,7 +10684,9 @@ static void setSetupInfoToDefaults(struct SetupInfo *si)
   si->game_frame_delay = GAME_FRAME_DELAY;
   si->sp_show_border_elements = FALSE;
   si->small_game_graphics = FALSE;
-  si->show_snapshot_buttons = FALSE;
+  si->show_load_save_buttons = FALSE;
+  si->show_undo_redo_buttons = FALSE;
+  si->scores_in_highscore_list = getStringCopy(STR_SCORES_TYPE_DEFAULT);
 
   si->graphics_set = getStringCopy(GFX_CLASSIC_SUBDIR);
   si->sounds_set   = getStringCopy(SND_CLASSIC_SUBDIR);
@@ -9539,6 +10910,20 @@ static void setSetupInfoToDefaults_AutoSetup(struct SetupInfo *si)
   si->auto_setup.editor_zoom_tilesize = MINI_TILESIZE;
 }
 
+static void setSetupInfoToDefaults_ServerSetup(struct SetupInfo *si)
+{
+  si->player_uuid = NULL;      // (will be set later)
+
+  si->use_api_server = TRUE;
+  si->api_server_hostname = getStringCopy(API_SERVER_HOSTNAME);
+  si->api_server_password = getStringCopy(UNDEFINED_PASSWORD);
+  si->ask_for_uploading_tapes = TRUE;
+  si->ask_for_remaining_tapes = FALSE;
+  si->provide_uploading_tapes = TRUE;
+  si->ask_for_using_api_server = TRUE;
+  si->has_remaining_tapes = FALSE;
+}
+
 static void setSetupInfoToDefaults_EditorCascade(struct SetupInfo *si)
 {
   si->editor_cascade.el_bd             = TRUE;
@@ -9626,7 +11011,7 @@ static void setSetupInfoFromTokenInfo(SetupFileHash *setup_file_hash,
                            token_info[token_nr].text);
 }
 
-static void decodeSetupFileHash(SetupFileHash *setup_file_hash)
+static void decodeSetupFileHash_Default(SetupFileHash *setup_file_hash)
 {
   int i, pnr;
 
@@ -9726,6 +11111,19 @@ static void decodeSetupFileHash_AutoSetup(SetupFileHash *setup_file_hash)
                              auto_setup_tokens[i].text));
 }
 
+static void decodeSetupFileHash_ServerSetup(SetupFileHash *setup_file_hash)
+{
+  int i;
+
+  if (!setup_file_hash)
+    return;
+
+  for (i = 0; i < ARRAY_SIZE(server_setup_tokens); i++)
+    setSetupInfo(server_setup_tokens, i,
+                getHashEntry(setup_file_hash,
+                             server_setup_tokens[i].text));
+}
+
 static void decodeSetupFileHash_EditorCascade(SetupFileHash *setup_file_hash)
 {
   int i;
@@ -9782,7 +11180,7 @@ void LoadSetupFromFilename(char *filename)
 
   if (setup_file_hash)
   {
-    decodeSetupFileHash(setup_file_hash);
+    decodeSetupFileHash_Default(setup_file_hash);
 
     freeSetupFileHash(setup_file_hash);
   }
@@ -9813,7 +11211,7 @@ static void LoadSetup_SpecialPostProcessing(void)
     MIN(MAX(MIN_SCROLL_DELAY, setup.scroll_delay_value), MAX_SCROLL_DELAY);
 }
 
-void LoadSetup(void)
+void LoadSetup_Default(void)
 {
   char *filename;
 
@@ -9854,6 +11252,34 @@ void LoadSetup_AutoSetup(void)
   free(filename);
 }
 
+void LoadSetup_ServerSetup(void)
+{
+  char *filename = getPath2(getSetupDir(), SERVERSETUP_FILENAME);
+  SetupFileHash *setup_file_hash = NULL;
+
+  // always start with reliable default values
+  setSetupInfoToDefaults_ServerSetup(&setup);
+
+  setup_file_hash = loadSetupFileHash(filename);
+
+  if (setup_file_hash)
+  {
+    decodeSetupFileHash_ServerSetup(setup_file_hash);
+
+    freeSetupFileHash(setup_file_hash);
+  }
+
+  free(filename);
+
+  if (setup.player_uuid == NULL)
+  {
+    // player UUID does not yet exist in setup file
+    setup.player_uuid = getStringCopy(getUUID());
+
+    SaveSetup_ServerSetup();
+  }
+}
+
 void LoadSetup_EditorCascade(void)
 {
   char *filename = getPath2(getSetupDir(), EDITORCASCADE_FILENAME);
@@ -9874,6 +11300,14 @@ void LoadSetup_EditorCascade(void)
   free(filename);
 }
 
+void LoadSetup(void)
+{
+  LoadSetup_Default();
+  LoadSetup_AutoSetup();
+  LoadSetup_ServerSetup();
+  LoadSetup_EditorCascade();
+}
+
 static void addGameControllerMappingToHash(SetupFileHash *mappings_hash,
                                           char *mapping_line)
 {
@@ -9923,7 +11357,7 @@ static void LoadSetup_ReadGameControllerMappings(SetupFileHash *mappings_hash,
   fclose(file);
 }
 
-void SaveSetup(void)
+void SaveSetup_Default(void)
 {
   char *filename = getSetupFilename();
   FILE *file;
@@ -10054,6 +11488,41 @@ void SaveSetup_AutoSetup(void)
   free(filename);
 }
 
+void SaveSetup_ServerSetup(void)
+{
+  char *filename = getPath2(getSetupDir(), SERVERSETUP_FILENAME);
+  FILE *file;
+  int i;
+
+  InitUserDataDirectory();
+
+  if (!(file = fopen(filename, MODE_WRITE)))
+  {
+    Warn("cannot write server setup file '%s'", filename);
+
+    free(filename);
+
+    return;
+  }
+
+  fprintFileHeader(file, SERVERSETUP_FILENAME);
+
+  for (i = 0; i < ARRAY_SIZE(server_setup_tokens); i++)
+  {
+    // just to make things nicer :)
+    if (server_setup_tokens[i].value == &setup.use_api_server)
+      fprintf(file, "\n");
+
+    fprintf(file, "%s\n", getSetupLine(server_setup_tokens, "", i));
+  }
+
+  fclose(file);
+
+  SetFilePermissions(filename, PERMS_PRIVATE);
+
+  free(filename);
+}
+
 void SaveSetup_EditorCascade(void)
 {
   char *filename = getPath2(getSetupDir(), EDITORCASCADE_FILENAME);
@@ -10083,6 +11552,14 @@ void SaveSetup_EditorCascade(void)
   free(filename);
 }
 
+void SaveSetup(void)
+{
+  SaveSetup_Default();
+  SaveSetup_AutoSetup();
+  SaveSetup_ServerSetup();
+  SaveSetup_EditorCascade();
+}
+
 static void SaveSetup_WriteGameControllerMappings(SetupFileHash *mappings_hash,
                                                  char *filename)
 {
@@ -12132,6 +13609,11 @@ void ConvertLevels(void)
 
     Print("converting level ... ");
 
+#if 0
+    // special case: conversion of some EMC levels as requested by ACME
+    level.game_engine_type = GAME_ENGINE_TYPE_RND;
+#endif
+
     level_filename = getDefaultLevelFilename(level_nr);
     new_level = !fileExists(level_filename);
 
@@ -12209,8 +13691,8 @@ void CreateLevelSketchImages(void)
     sprintf(basename1, "%04d.bmp", i);
     sprintf(basename2, "%04ds.bmp", i);
 
-    filename1 = getPath2(global.create_images_dir, basename1);
-    filename2 = getPath2(global.create_images_dir, basename2);
+    filename1 = getPath2(global.create_sketch_images_dir, basename1);
+    filename2 = getPath2(global.create_sketch_images_dir, basename2);
 
     DrawSizedElement(0, 0, element, TILESIZE);
     BlitBitmap(drawto, bitmap1, SX, SY, TILEX, TILEY, 0, 0);
@@ -12254,6 +13736,104 @@ void CreateLevelSketchImages(void)
 }
 
 
+// ----------------------------------------------------------------------------
+// create and save images for element collecting animations (raw BMP format)
+// ----------------------------------------------------------------------------
+
+static boolean createCollectImage(int element)
+{
+  return (IS_COLLECTIBLE(element) && !IS_SP_ELEMENT(element));
+}
+
+void CreateCollectElementImages(void)
+{
+  int i, j;
+  int num_steps = 8;
+  int anim_frames = num_steps - 1;
+  int tile_size = TILESIZE;
+  int anim_width  = tile_size * anim_frames;
+  int anim_height = tile_size;
+  int num_collect_images = 0;
+  int pos_collect_images = 0;
+
+  for (i = 0; i < MAX_NUM_ELEMENTS; i++)
+    if (createCollectImage(i))
+      num_collect_images++;
+
+  Info("Creating %d element collecting animation images ...",
+       num_collect_images);
+
+  int dst_width  = anim_width * 2;
+  int dst_height = anim_height * num_collect_images / 2;
+  Bitmap *dst_bitmap = CreateBitmap(dst_width, dst_height, DEFAULT_DEPTH);
+  char *basename = "RocksCollect.bmp";
+  char *filename = getPath2(global.create_collect_images_dir, basename);
+
+  for (i = 0; i < MAX_NUM_ELEMENTS; i++)
+  {
+    if (!createCollectImage(i))
+      continue;
+
+    int dst_x = (pos_collect_images / (num_collect_images / 2)) * anim_width;
+    int dst_y = (pos_collect_images % (num_collect_images / 2)) * anim_height;
+    int graphic = el2img(i);
+    char *token_name = element_info[i].token_name;
+    Bitmap *tmp_bitmap = CreateBitmap(tile_size, tile_size, DEFAULT_DEPTH);
+    Bitmap *src_bitmap;
+    int src_x, src_y;
+
+    Info("- creating collecting image for '%s' ...", token_name);
+
+    getGraphicSource(graphic, 0, &src_bitmap, &src_x, &src_y);
+
+    BlitBitmap(src_bitmap, tmp_bitmap, src_x, src_y,
+              tile_size, tile_size, 0, 0);
+
+    tmp_bitmap->surface_masked = tmp_bitmap->surface;
+
+    for (j = 0; j < anim_frames; j++)
+    {
+      int frame_size_final = tile_size * (anim_frames - j) / num_steps;
+      int frame_size = frame_size_final * num_steps;
+      int offset = (tile_size - frame_size_final) / 2;
+      Bitmap *frame_bitmap = ZoomBitmap(tmp_bitmap, frame_size, frame_size);
+
+      while (frame_size > frame_size_final)
+      {
+       frame_size /= 2;
+
+       Bitmap *half_bitmap = ZoomBitmap(frame_bitmap, frame_size, frame_size);
+
+       FreeBitmap(frame_bitmap);
+
+       frame_bitmap = half_bitmap;
+      }
+
+      BlitBitmap(frame_bitmap, dst_bitmap, 0, 0,
+                frame_size_final, frame_size_final,
+                dst_x + j * tile_size + offset, dst_y + offset);
+
+      FreeBitmap(frame_bitmap);
+    }
+
+    tmp_bitmap->surface_masked = NULL;
+
+    FreeBitmap(tmp_bitmap);
+
+    pos_collect_images++;
+  }
+
+  if (SDL_SaveBMP(dst_bitmap->surface, filename) != 0)
+    Fail("cannot save element collecting image file '%s'", filename);
+
+  FreeBitmap(dst_bitmap);
+
+  Info("Done.");
+
+  CloseAllAndExit(0);
+}
+
+
 // ----------------------------------------------------------------------------
 // create and save images for custom and group elements (raw BMP format)
 // ----------------------------------------------------------------------------
index b82c89e..e32fcd8 100644 (file)
@@ -35,6 +35,9 @@ char *getGlobalLevelTemplateFilename(void);
 
 int getMappedElement(int);
 
+void ExecuteAsThread(SDL_ThreadFunction, char *, void *, char *);
+char *getPasswordJSON(char *);
+
 void LoadLevelFromFilename(struct LevelInfo *, char *);
 void LoadLevel(int);
 void LoadLevelTemplate(int);
@@ -44,6 +47,7 @@ void SaveLevel(int);
 void SaveLevelTemplate(void);
 void SaveNativeLevel(struct LevelInfo *);
 void DumpLevel(struct LevelInfo *);
+void DumpLevels(void);
 boolean SaveLevelChecked(int);
 
 void CopyNativeLevel_RND_to_Native(struct LevelInfo *);
@@ -54,25 +58,39 @@ void LoadTape(int);
 void LoadSolutionTape(int);
 void SaveTapeToFilename(char *);
 void SaveTape(int);
+void SaveScoreTape(int);
 void DumpTape(struct TapeInfo *);
+void DumpTapes(void);
 boolean SaveTapeChecked(int);
 boolean SaveTapeChecked_LevelSolved(int);
 
 void LoadScore(int);
 void SaveScore(int);
 
+void LoadServerScore(int, boolean);
+void SaveServerScore(int, boolean);
+void SaveServerScoreFromFile(int, boolean, char *);
+
+void LoadLocalAndServerScore(int, boolean);
+
 void LoadUserNames(void);
 
 void LoadSetupFromFilename(char *);
-void LoadSetup(void);
-void SaveSetup(void);
+void LoadSetup_Default(void);
+void SaveSetup_Default(void);
 
 void LoadSetup_AutoSetup(void);
 void SaveSetup_AutoSetup(void);
 
+void LoadSetup_ServerSetup(void);
+void SaveSetup_ServerSetup(void);
+
 void LoadSetup_EditorCascade(void);
 void SaveSetup_EditorCascade(void);
 
+void LoadSetup(void);
+void SaveSetup(void);
+
 void SaveSetup_AddGameControllerMapping(char *);
 
 void setHideSetupEntry(void *);
@@ -90,6 +108,7 @@ void LoadHelpTextInfo(void);
 
 void ConvertLevels(void);
 void CreateLevelSketchImages(void);
+void CreateCollectElementImages(void);
 void CreateCustomElementImages(char *);
 
 void FreeGlobalAnimEventInfo(void);
index 07df1fe..d4cc5ea 100644 (file)
@@ -1106,7 +1106,7 @@ void ContinueMoving(int, int);
 void Bang(int, int);
 void InitMovDir(int, int);
 void InitAmoebaNr(int, int);
-int NewHiScore(int);
+void NewHighScore(int, boolean);
 
 void TestIfGoodThingHitsBadThing(int, int, int);
 void TestIfBadThingHitsGoodThing(int, int, int);
@@ -2402,7 +2402,7 @@ static void UpdateGameControlValues(void)
   }
 
   game_panel_controls[GAME_PANEL_SCORE].value = score;
-  game_panel_controls[GAME_PANEL_HIGHSCORE].value = highscore[0].Score;
+  game_panel_controls[GAME_PANEL_HIGHSCORE].value = scores.entry[0].score;
 
   game_panel_controls[GAME_PANEL_TIME].value = time;
 
@@ -3542,7 +3542,6 @@ void InitGame(void)
   int fade_mask = REDRAW_FIELD;
 
   boolean emulate_bd = TRUE;   // unless non-BOULDERDASH elements found
-  boolean emulate_sb = TRUE;   // unless non-SOKOBAN     elements found
   boolean emulate_sp = TRUE;   // unless non-SUPAPLEX    elements found
   int initial_move_dir = MV_DOWN;
   int i, j, x, y;
@@ -3809,6 +3808,9 @@ void InitGame(void)
   game.switchgate_pos = 0;
   game.wind_direction = level.wind_direction_initial;
 
+  game.time_final = 0;
+  game.score_time_final = 0;
+
   game.score = 0;
   game.score_final = 0;
 
@@ -3883,8 +3885,6 @@ void InitGame(void)
   {
     if (emulate_bd && !IS_BD_ELEMENT(Tile[x][y]))
       emulate_bd = FALSE;
-    if (emulate_sb && !IS_SB_ELEMENT(Tile[x][y]))
-      emulate_sb = FALSE;
     if (emulate_sp && !IS_SP_ELEMENT(Tile[x][y]))
       emulate_sp = FALSE;
 
@@ -3909,7 +3909,6 @@ void InitGame(void)
   }
 
   game.emulation = (emulate_bd ? EMU_BOULDERDASH :
-                   emulate_sb ? EMU_SOKOBAN :
                    emulate_sp ? EMU_SUPAPLEX : EMU_NONE);
 
   // initialize type of slippery elements
@@ -4307,7 +4306,7 @@ void InitGame(void)
        {
          // check for player created from custom element as single target
          content = element_info[element].change_page[i].target_element;
-         is_player = ELEM_IS_PLAYER(content);
+         is_player = IS_PLAYER_ELEMENT(content);
 
          if (is_player && (found_rating < 3 ||
                            (found_rating == 3 && element < found_element)))
@@ -4325,7 +4324,7 @@ void InitGame(void)
       {
        // check for player created from custom element as explosion content
        content = element_info[element].content.e[xx][yy];
-       is_player = ELEM_IS_PLAYER(content);
+       is_player = IS_PLAYER_ELEMENT(content);
 
        if (is_player && (found_rating < 2 ||
                          (found_rating == 2 && element < found_element)))
@@ -4346,7 +4345,7 @@ void InitGame(void)
          content =
            element_info[element].change_page[i].target_content.e[xx][yy];
 
-         is_player = ELEM_IS_PLAYER(content);
+         is_player = IS_PLAYER_ELEMENT(content);
 
          if (is_player && (found_rating < 1 ||
                            (found_rating == 1 && element < found_element)))
@@ -4697,29 +4696,53 @@ void InitAmoebaNr(int x, int y)
   AmoebaCnt2[group_nr]++;
 }
 
-static void LevelSolved(void)
+static void LevelSolved_SetFinalGameValues(void)
 {
-  if (level.game_engine_type == GAME_ENGINE_TYPE_RND &&
-      game.players_still_needed > 0)
-    return;
-
-  game.LevelSolved = TRUE;
-  game.GameOver = TRUE;
+  game.time_final = (game.no_time_limit ? TimePlayed : TimeLeft);
+  game.score_time_final = (level.use_step_counter ? TimePlayed :
+                          TimePlayed * FRAMES_PER_SECOND + TimeFrames);
 
   game.score_final = (level.game_engine_type == GAME_ENGINE_TYPE_EM ?
                      game_em.lev->score :
                      level.game_engine_type == GAME_ENGINE_TYPE_MM ?
                      game_mm.score :
                      game.score);
+
   game.health_final = (level.game_engine_type == GAME_ENGINE_TYPE_MM ?
                       MM_HEALTH(game_mm.laser_overload_value) :
                       game.health);
 
-  game.LevelSolved_CountingTime = (game.no_time_limit ? TimePlayed : TimeLeft);
+  game.LevelSolved_CountingTime = game.time_final;
   game.LevelSolved_CountingScore = game.score_final;
   game.LevelSolved_CountingHealth = game.health_final;
 }
 
+static void LevelSolved_DisplayFinalGameValues(int time, int score, int health)
+{
+  game.LevelSolved_CountingTime = time;
+  game.LevelSolved_CountingScore = score;
+  game.LevelSolved_CountingHealth = health;
+
+  game_panel_controls[GAME_PANEL_TIME].value = time;
+  game_panel_controls[GAME_PANEL_SCORE].value = score;
+  game_panel_controls[GAME_PANEL_HEALTH].value = health;
+
+  DisplayGameControlValues();
+}
+
+static void LevelSolved(void)
+{
+  if (level.game_engine_type == GAME_ENGINE_TYPE_RND &&
+      game.players_still_needed > 0)
+    return;
+
+  game.LevelSolved = TRUE;
+  game.GameOver = TRUE;
+
+  // needed here to display correct panel values while player walks into exit
+  LevelSolved_SetFinalGameValues();
+}
+
 void GameWon(void)
 {
   static int time_count_steps;
@@ -4740,6 +4763,9 @@ void GameWon(void)
     if (local_player->active && local_player->MovPos)
       return;
 
+    // calculate final game values after player finished walking into exit
+    LevelSolved_SetFinalGameValues();
+
     game.LevelSolved_GameWon = TRUE;
     game.LevelSolved_SaveTape = tape.recording;
     game.LevelSolved_SaveScore = !tape.playing;
@@ -4760,23 +4786,31 @@ void GameWon(void)
     game_over_delay_2 = FRAMES_PER_SECOND / 2; // delay before counting health
     game_over_delay_3 = FRAMES_PER_SECOND;     // delay before ending the game
 
-    time = time_final = (game.no_time_limit ? TimePlayed : TimeLeft);
+    time = time_final = game.time_final;
     score = score_final = game.score_final;
     health = health_final = game.health_final;
 
+    // update game panel values before (delayed) counting of score (if any)
+    LevelSolved_DisplayFinalGameValues(time, score, health);
+
+    // if level has time score defined, calculate new final game values
     if (time_score > 0)
     {
+      int time_final_max = 999;
+      int time_frames_final_max = time_final_max * FRAMES_PER_SECOND;
       int time_frames = 0;
+      int time_frames_left = TimeLeft * FRAMES_PER_SECOND - TimeFrames;
+      int time_frames_played = TimePlayed * FRAMES_PER_SECOND + TimeFrames;
 
       if (TimeLeft > 0)
       {
        time_final = 0;
-       time_frames = TimeLeft * FRAMES_PER_SECOND - TimeFrames;
+       time_frames = time_frames_left;
       }
-      else if (game.no_time_limit && TimePlayed < 999)
+      else if (game.no_time_limit && TimePlayed < time_final_max)
       {
-       time_final = 999;
-       time_frames = (999 - TimePlayed) * FRAMES_PER_SECOND - TimeFrames;
+       time_final = time_final_max;
+       time_frames = time_frames_final_max - time_frames_played;
       }
 
       score_final += time_score * time_frames / FRAMES_PER_SECOND + 0.5;
@@ -4793,18 +4827,13 @@ void GameWon(void)
       game.health_final = health_final;
     }
 
+    // if not counting score after game, immediately update game panel values
     if (level_editor_test_game || !setup.count_score_after_game)
     {
       time = time_final;
       score = score_final;
 
-      game.LevelSolved_CountingTime = time;
-      game.LevelSolved_CountingScore = score;
-
-      game_panel_controls[GAME_PANEL_TIME].value = time;
-      game_panel_controls[GAME_PANEL_SCORE].value = score;
-
-      DisplayGameControlValues();
+      LevelSolved_DisplayFinalGameValues(time, score, health);
     }
 
     if (level.game_engine_type == GAME_ENGINE_TYPE_RND)
@@ -4881,13 +4910,7 @@ void GameWon(void)
       if (time == time_final)
        score = score_final;
 
-      game.LevelSolved_CountingTime = time;
-      game.LevelSolved_CountingScore = score;
-
-      game_panel_controls[GAME_PANEL_TIME].value = time;
-      game_panel_controls[GAME_PANEL_SCORE].value = score;
-
-      DisplayGameControlValues();
+      LevelSolved_DisplayFinalGameValues(time, score, health);
 
       if (time == time_final)
        StopSound(SND_GAME_LEVELTIME_BONUS);
@@ -4913,13 +4936,7 @@ void GameWon(void)
       health += health_count_dir;
       score  += time_score;
 
-      game.LevelSolved_CountingHealth = health;
-      game.LevelSolved_CountingScore = score;
-
-      game_panel_controls[GAME_PANEL_HEALTH].value = health;
-      game_panel_controls[GAME_PANEL_SCORE].value = score;
-
-      DisplayGameControlValues();
+      LevelSolved_DisplayFinalGameValues(time, score, health);
 
       if (health == health_final)
        StopSound(SND_GAME_LEVELTIME_BONUS);
@@ -4948,7 +4965,7 @@ void GameEnd(void)
 {
   // used instead of "level_nr" (needed for network games)
   int last_level_nr = levelset.level_nr;
-  int hi_pos;
+  boolean tape_saved = FALSE;
 
   game.LevelSolved_GameEnd = TRUE;
 
@@ -4958,7 +4975,11 @@ void GameEnd(void)
     if (!global.use_envelope_request)
       CloseDoor(DOOR_CLOSE_1);
 
-    SaveTapeChecked_LevelSolved(tape.level_nr);                // ask to save tape
+    // ask to save tape
+    tape_saved = SaveTapeChecked_LevelSolved(tape.level_nr);
+
+    // set unique basename for score tape (also saved in high score table)
+    strcpy(tape.score_tape_basename, getScoreTapeBasename(setup.player_name));
   }
 
   // if no tape is to be saved, close both doors simultaneously
@@ -4989,6 +5010,9 @@ void GameEnd(void)
     SaveLevelSetup_SeriesInfo();
   }
 
+  // save score and score tape before potentially erasing tape below
+  NewHighScore(last_level_nr, tape_saved);
+
   if (setup.increment_levels &&
       level_nr < leveldir_current->last_level &&
       !network_playing)
@@ -5004,13 +5028,11 @@ void GameEnd(void)
     }
   }
 
-  hi_pos = NewHiScore(last_level_nr);
-
-  if (hi_pos >= 0 && setup.show_scores_after_game)
+  if (scores.last_added >= 0 && setup.show_scores_after_game)
   {
     SetGameStatus(GAME_MODE_SCORES);
 
-    DrawHallOfFame(last_level_nr, hi_pos);
+    DrawHallOfFame(last_level_nr);
   }
   else if (setup.auto_play_next_level && setup.increment_levels &&
           last_level_nr < leveldir_current->last_level &&
@@ -5026,64 +5048,149 @@ void GameEnd(void)
   }
 }
 
-int NewHiScore(int level_nr)
+static int addScoreEntry(struct ScoreInfo *list, struct ScoreEntry *new_entry,
+                        boolean one_score_entry_per_name)
 {
-  int k, l;
-  int position = -1;
-  boolean one_score_entry_per_name = !program.many_scores_per_name;
-
-  LoadScore(level_nr);
+  int i;
 
-  if (strEqual(setup.player_name, EMPTY_PLAYER_NAME) ||
-      game.score_final < highscore[MAX_SCORE_ENTRIES - 1].Score)
+  if (strEqual(new_entry->name, EMPTY_PLAYER_NAME))
     return -1;
 
-  for (k = 0; k < MAX_SCORE_ENTRIES; k++)
-  {
-    if (game.score_final > highscore[k].Score)
+  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+  {
+    struct ScoreEntry *entry = &list->entry[i];
+    boolean score_is_better = (new_entry->score >  entry->score);
+    boolean score_is_equal  = (new_entry->score == entry->score);
+    boolean time_is_better  = (new_entry->time  <  entry->time);
+    boolean time_is_equal   = (new_entry->time  == entry->time);
+    boolean better_by_score = (score_is_better ||
+                              (score_is_equal && time_is_better));
+    boolean better_by_time  = (time_is_better ||
+                              (time_is_equal && score_is_better));
+    boolean is_better = (level.rate_time_over_score ? better_by_time :
+                        better_by_score);
+    boolean entry_is_empty = (entry->score == 0 &&
+                             entry->time == 0);
+
+    // prevent adding server score entries if also existing in local score file
+    // (special case: historic score entries have an empty tape basename entry)
+    if (strEqual(new_entry->tape_basename, entry->tape_basename) &&
+       !strEqual(new_entry->tape_basename, UNDEFINED_FILENAME))
+      return -1;
+
+    if (is_better || entry_is_empty)
     {
       // player has made it to the hall of fame
 
-      if (k < MAX_SCORE_ENTRIES - 1)
+      if (i < MAX_SCORE_ENTRIES - 1)
       {
        int m = MAX_SCORE_ENTRIES - 1;
+       int l;
 
        if (one_score_entry_per_name)
        {
-         for (l = k; l < MAX_SCORE_ENTRIES; l++)
-           if (strEqual(setup.player_name, highscore[l].Name))
+         for (l = i; l < MAX_SCORE_ENTRIES; l++)
+           if (strEqual(list->entry[l].name, new_entry->name))
              m = l;
 
-         if (m == k)   // player's new highscore overwrites his old one
+         if (m == i)   // player's new highscore overwrites his old one
            goto put_into_list;
        }
 
-       for (l = m; l > k; l--)
-       {
-         strcpy(highscore[l].Name, highscore[l - 1].Name);
-         highscore[l].Score = highscore[l - 1].Score;
-       }
+       for (l = m; l > i; l--)
+         list->entry[l] = list->entry[l - 1];
       }
 
       put_into_list:
 
-      strncpy(highscore[k].Name, setup.player_name, MAX_PLAYER_NAME_LEN);
-      highscore[k].Name[MAX_PLAYER_NAME_LEN] = '\0';
-      highscore[k].Score = game.score_final;
-      position = k;
+      *entry = *new_entry;
 
-      break;
+      return i;
     }
     else if (one_score_entry_per_name &&
-            !strncmp(setup.player_name, highscore[k].Name,
-                     MAX_PLAYER_NAME_LEN))
-      break;   // player already there with a higher score
+            strEqual(entry->name, new_entry->name))
+    {
+      // player already in high score list with better score or time
+
+      return -1;
+    }
   }
 
-  if (position >= 0) 
-    SaveScore(level_nr);
+  return -1;
+}
+
+void NewHighScore(int level_nr, boolean tape_saved)
+{
+  struct ScoreEntry new_entry = {{ 0 }}; // (prevent warning from GCC bug 53119)
+  boolean one_per_name = FALSE;
+
+  strncpy(new_entry.tape_basename, tape.score_tape_basename, MAX_FILENAME_LEN);
+  strncpy(new_entry.name, setup.player_name, MAX_PLAYER_NAME_LEN);
+
+  new_entry.score = game.score_final;
+  new_entry.time = game.score_time_final;
 
-  return position;
+  LoadScore(level_nr);
+
+  scores.last_added = addScoreEntry(&scores, &new_entry, one_per_name);
+
+  if (scores.last_added < 0)
+    return;
+
+  SaveScore(level_nr);
+
+  // store last added local score entry (before merging server scores)
+  scores.last_added_local = scores.last_added;
+
+  if (!game.LevelSolved_SaveTape)
+    return;
+
+  SaveScoreTape(level_nr);
+
+  if (setup.ask_for_using_api_server)
+  {
+    setup.use_api_server =
+      Request("Upload your score and tape to the high score server?", REQ_ASK);
+
+    if (!setup.use_api_server)
+      Request("Not using high score server! Use setup menu to enable again!",
+             REQ_CONFIRM);
+
+    runtime.use_api_server = setup.use_api_server;
+
+    // after asking for using API server once, do not ask again
+    setup.ask_for_using_api_server = FALSE;
+
+    SaveSetup_ServerSetup();
+  }
+
+  SaveServerScore(level_nr, tape_saved);
+}
+
+void MergeServerScore(void)
+{
+  struct ScoreEntry last_added_entry;
+  boolean one_per_name = FALSE;
+  int i;
+
+  if (scores.last_added >= 0)
+    last_added_entry = scores.entry[scores.last_added];
+
+  for (i = 0; i < server_scores.num_entries; i++)
+  {
+    int pos = addScoreEntry(&scores, &server_scores.entry[i], one_per_name);
+
+    if (pos >= 0 && pos <= scores.last_added)
+      scores.last_added++;
+  }
+
+  if (scores.last_added >= MAX_SCORE_ENTRIES)
+  {
+    scores.last_added = MAX_SCORE_ENTRIES - 1;
+    scores.force_last_added = TRUE;
+
+    scores.entry[scores.last_added] = last_added_entry;
+  }
 }
 
 static int getElementMoveStepsizeExt(int x, int y, int direction)
@@ -5606,7 +5713,7 @@ static void RelocatePlayer(int jx, int jy, int el_player_raw)
      possible that the relocation target field did not contain a player element,
      but a walkable element, to which the new player was relocated -- in this
      case, restore that (already initialized!) element on the player field */
-  if (!ELEM_IS_PLAYER(element))        // player may be set on walkable element
+  if (!IS_PLAYER_ELEMENT(element))     // player may be set on walkable element
   {
     Tile[jx][jy] = element;    // restore previously existing element
   }
@@ -5776,7 +5883,7 @@ static void Explode(int ex, int ey, int phase, int mode)
 
       // !!! check this case -- currently needed for rnd_rado_negundo_v,
       // !!! levels 015 018 019 020 021 022 023 026 027 028 !!!
-      else if (ELEM_IS_PLAYER(center_element))
+      else if (IS_PLAYER_ELEMENT(center_element))
        Store[x][y] = EL_EMPTY;
       else if (center_element == EL_YAMYAM)
        Store[x][y] = level.yamyam_content[game.yamyam_content_nr].e[xx][yy];
@@ -5916,7 +6023,7 @@ static void Explode(int ex, int ey, int phase, int mode)
     if (IS_PLAYER(x, y) && !PLAYERINFO(x, y)->present)
       StorePlayer[x][y] = 0;
 
-    if (ELEM_IS_PLAYER(element))
+    if (IS_PLAYER_ELEMENT(element))
       RelocatePlayer(x, y, element);
   }
   else if (IN_SCR_FIELD(SCREENX(x), SCREENY(y)))
@@ -8702,7 +8809,7 @@ void ContinueMoving(int x, int y)
     if (GFX_CRUMBLED(Tile[x][y]))
       TEST_DrawLevelFieldCrumbledNeighbours(x, y);
 
-    if (ELEM_IS_PLAYER(move_leave_element))
+    if (IS_PLAYER_ELEMENT(move_leave_element))
       RelocatePlayer(x, y, move_leave_element);
   }
 
@@ -10512,7 +10619,7 @@ static void CreateFieldExt(int x, int y, int element, boolean is_change)
   int previous_move_direction = MovDir[x][y];
   int last_ce_value = CustomValue[x][y];
   boolean player_explosion_protected = PLAYER_EXPLOSION_PROTECTED(x, y);
-  boolean new_element_is_player = ELEM_IS_PLAYER(new_element);
+  boolean new_element_is_player = IS_PLAYER_ELEMENT(new_element);
   boolean add_player_onto_element = (new_element_is_player &&
                                     new_element != EL_SOKOBAN_FIELD_PLAYER &&
                                     IS_WALKABLE(old_element));
@@ -10688,7 +10795,7 @@ static boolean ChangeElement(int x, int y, int element, int page)
          (change->replace_when == CP_WHEN_COLLECTIBLE  && is_collectible) ||
          (change->replace_when == CP_WHEN_REMOVABLE    && is_removable) ||
          (change->replace_when == CP_WHEN_DESTRUCTIBLE && is_destructible)) &&
-        !(IS_PLAYER(ex, ey) && ELEM_IS_PLAYER(content_element)));
+        !(IS_PLAYER(ex, ey) && IS_PLAYER_ELEMENT(content_element)));
 
       if (!can_replace[xx][yy])
        complete_replace = FALSE;
@@ -10750,6 +10857,10 @@ static boolean ChangeElement(int x, int y, int element, int page)
       Store[x][y] = EL_EMPTY;
     }
 
+    // special case: element changes to player (and may be kept if walkable)
+    if (IS_PLAYER_ELEMENT(target_element) && !level.keep_walkable_ce)
+      CreateElementFromChange(x, y, EL_EMPTY);
+
     CreateElementFromChange(x, y, target_element);
 
     PlayLevelSoundElementAction(x, y, element, ACTION_CHANGING);
@@ -11620,7 +11731,7 @@ static void GameActionsExt(void)
     Warn("element '%s' caused endless loop in game engine",
         EL_NAME(recursion_loop_element));
 
-    RequestQuitGameExt(FALSE, level_editor_test_game, message);
+    RequestQuitGameExt(program.headless, level_editor_test_game, message);
 
     recursion_loop_detected = FALSE;   // if game should be continued
 
@@ -12091,6 +12202,9 @@ void GameActions_RND(void)
        TEST_DrawLevelField(x, y);
 
        TestFieldAfterSnapping(x, y, element, move_direction, player_index_bit);
+
+       if (IS_ENVELOPE(element))
+         local_player->show_envelope = element;
       }
     }
 
@@ -13113,7 +13227,7 @@ void ScrollPlayer(struct PlayerInfo *player, int mode)
        RemovePlayer(player);
     }
 
-    if (!game.LevelSolved && level.use_step_counter)
+    if (level.use_step_counter)
     {
       int i;
 
@@ -13123,14 +13237,14 @@ void ScrollPlayer(struct PlayerInfo *player, int mode)
       {
        TimeLeft--;
 
-       if (TimeLeft <= 10 && setup.time_limit)
+       if (TimeLeft <= 10 && setup.time_limit && !game.LevelSolved)
          PlaySound(SND_GAME_RUNNING_OUT_OF_TIME);
 
        game_panel_controls[GAME_PANEL_TIME].value = TimeLeft;
 
        DisplayGameControlValues();
 
-       if (!TimeLeft && setup.time_limit)
+       if (!TimeLeft && setup.time_limit && !game.LevelSolved)
          for (i = 0; i < MAX_PLAYERS; i++)
            KillPlayer(&stored_player[i]);
       }
@@ -13874,7 +13988,11 @@ static void TestFieldAfterSnapping(int x, int y, int element, int direction,
   if (level.finish_dig_collect)
   {
     int dig_side = MV_DIR_OPPOSITE(direction);
+    int change_event = (IS_DIGGABLE(element) ? CE_PLAYER_DIGS_X :
+                       CE_PLAYER_COLLECTS_X);
 
+    CheckTriggeredElementChangeByPlayer(x, y, element, change_event,
+                                       player_index_bit, dig_side);
     CheckTriggeredElementChangeByPlayer(x, y, element, CE_PLAYER_SNAPS_X,
                                        player_index_bit, dig_side);
   }
@@ -14248,7 +14366,10 @@ static int DigField(struct PlayerInfo *player,
     }
     else if (IS_ENVELOPE(element))
     {
-      player->show_envelope = element;
+      boolean wait_for_snapping = (mode == DF_SNAP && level.block_snap_field);
+
+      if (!wait_for_snapping)
+       player->show_envelope = element;
     }
     else if (element == EL_EMC_LENSES)
     {
@@ -14435,7 +14556,7 @@ static int DigField(struct PlayerInfo *player,
       if (sokoban_task_solved &&
          game.sokoban_fields_still_needed == 0 &&
          game.sokoban_objects_still_needed == 0 &&
-         (game.emulation == EMU_SOKOBAN || level.auto_exit_sokoban))
+         level.auto_exit_sokoban)
       {
        game.players_still_needed = 0;
 
@@ -16065,12 +16186,18 @@ static void UnmapGameButtonsAtSamePosition(int id)
 
 static void UnmapGameButtonsAtSamePosition_All(void)
 {
-  if (setup.show_snapshot_buttons)
+  if (setup.show_load_save_buttons)
   {
     UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_SAVE);
     UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_PAUSE2);
     UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_LOAD);
   }
+  else if (setup.show_undo_redo_buttons)
+  {
+    UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_UNDO);
+    UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_PAUSE2);
+    UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_REDO);
+  }
   else
   {
     UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_STOP);
@@ -16083,17 +16210,13 @@ static void UnmapGameButtonsAtSamePosition_All(void)
   }
 }
 
-static void MapGameButtonsAtSamePosition(int id)
+void MapLoadSaveButtons(void)
 {
-  int i;
-
-  for (i = 0; i < NUM_GAME_BUTTONS; i++)
-    if (i != id &&
-       gamebutton_info[i].pos->x == gamebutton_info[id].pos->x &&
-       gamebutton_info[i].pos->y == gamebutton_info[id].pos->y)
-      MapGadget(game_gadget[i]);
+  UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_LOAD);
+  UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_SAVE);
 
-  UnmapGameButtonsAtSamePosition_All();
+  MapGadget(game_gadget[GAME_CTRL_ID_LOAD]);
+  MapGadget(game_gadget[GAME_CTRL_ID_SAVE]);
 }
 
 void MapUndoRedoButtons(void)
@@ -16105,15 +16228,6 @@ void MapUndoRedoButtons(void)
   MapGadget(game_gadget[GAME_CTRL_ID_REDO]);
 }
 
-void UnmapUndoRedoButtons(void)
-{
-  UnmapGadget(game_gadget[GAME_CTRL_ID_UNDO]);
-  UnmapGadget(game_gadget[GAME_CTRL_ID_REDO]);
-
-  MapGameButtonsAtSamePosition(GAME_CTRL_ID_UNDO);
-  MapGameButtonsAtSamePosition(GAME_CTRL_ID_REDO);
-}
-
 void ModifyPauseButtons(void)
 {
   static int ids[] =
@@ -16135,9 +16249,7 @@ static void MapGameButtonsExt(boolean on_tape)
   int i;
 
   for (i = 0; i < NUM_GAME_BUTTONS; i++)
-    if ((!on_tape || gamebutton_info[i].allowed_on_tape) &&
-       i != GAME_CTRL_ID_UNDO &&
-       i != GAME_CTRL_ID_REDO)
+    if (!on_tape || gamebutton_info[i].allowed_on_tape)
       MapGadget(game_gadget[i]);
 
   UnmapGameButtonsAtSamePosition_All();
@@ -16229,6 +16341,8 @@ static void GameUndoRedoExt(void)
   DrawVideoDisplay(VIDEO_STATE_FRAME_ON, FrameCounter);
   DrawVideoDisplay(VIDEO_STATE_1STEP(tape.single_step), 0);
 
+  ModifyPauseButtons();
+
   BackToFront();
 }
 
@@ -16237,8 +16351,12 @@ static void GameUndo(int steps)
   if (!CheckEngineSnapshotList())
     return;
 
+  int tape_property_bits = tape.property_bits;
+
   LoadEngineSnapshot_Undo(steps);
 
+  tape.property_bits |= tape_property_bits | TAPE_PROPERTY_SNAPSHOT;
+
   GameUndoRedoExt();
 }
 
@@ -16247,8 +16365,12 @@ static void GameRedo(int steps)
   if (!CheckEngineSnapshotList())
     return;
 
+  int tape_property_bits = tape.property_bits;
+
   LoadEngineSnapshot_Redo(steps);
 
+  tape.property_bits |= tape_property_bits | TAPE_PROPERTY_SNAPSHOT;
+
   GameUndoRedoExt();
 }
 
index 5739ecf..89ec940 100644 (file)
 #define STR_SNAPSHOT_MODE_EVERY_COLLECT        "every_collect"
 #define STR_SNAPSHOT_MODE_DEFAULT      STR_SNAPSHOT_MODE_OFF
 
+#define STR_SCORES_TYPE_LOCAL_ONLY      "local_scores_only"
+#define STR_SCORES_TYPE_SERVER_ONLY     "server_scores_only"
+#define STR_SCORES_TYPE_LOCAL_AND_SERVER "local_and_server_scores"
+#define STR_SCORES_TYPE_DEFAULT                 STR_SCORES_TYPE_LOCAL_AND_SERVER
+
 #define SNAPSHOT_MODE_OFF              0
 #define SNAPSHOT_MODE_EVERY_STEP       1
 #define SNAPSHOT_MODE_EVERY_MOVE       2
@@ -199,6 +204,9 @@ struct GameInfo
   boolean envelope_active;
   boolean no_time_limit;       // (variable only in very special case)
 
+  int time_final;              // time (in seconds) or steps left or played
+  int score_time_final;                // time (in frames) or steps played
+
   int score;
   int score_final;
 
@@ -414,6 +422,8 @@ void UpdateEngineValues(int, int, int, int);
 void GameWon(void);
 void GameEnd(void);
 
+void MergeServerScore(void);
+
 void InitPlayerGfxAnimation(struct PlayerInfo *, int, int);
 void Moving2Blocked(int, int, int *, int *);
 void Blocked2Moving(int, int, int *, int *);
@@ -465,8 +475,8 @@ boolean CheckEngineSnapshotList(void);
 
 void CreateGameButtons(void);
 void FreeGameButtons(void);
+void MapLoadSaveButtons(void);
 void MapUndoRedoButtons(void);
-void UnmapUndoRedoButtons(void);
 void ModifyPauseButtons(void);
 void MapGameButtons(void);
 void UnmapGameButtons(void);
index a157728..52f0b01 100644 (file)
@@ -3151,19 +3151,6 @@ static void GameActions_MM_Ext(struct MouseActionInfo action, boolean warp_mode)
 
       game.restart_game_message = "Out of magic energy! Play it again?";
 
-#if 0
-      if (Request("Out of magic energy! Play it again?",
-                 REQ_ASK | REQ_STAY_CLOSED))
-      {
-       InitGame();
-      }
-      else
-      {
-       game_status = MAINMENU;
-       DrawMainMenu();
-      }
-#endif
-
       return;
     }
   }
@@ -3284,19 +3271,6 @@ static void GameActions_MM_Ext(struct MouseActionInfo action, boolean warp_mode)
 
       game.restart_game_message = "Magic spell hit Mc Duffin! Play it again?";
 
-#if 0
-      if (Request("Magic spell hit Mc Duffin! Play it again?",
-                 REQ_ASK | REQ_STAY_CLOSED))
-      {
-       InitGame();
-      }
-      else
-      {
-       game_status = MAINMENU;
-       DrawMainMenu();
-      }
-#endif
-
       return;
     }
   }
@@ -3311,36 +3285,10 @@ static void GameActions_MM_Ext(struct MouseActionInfo action, boolean warp_mode)
     if (game_mm.cheat_no_explosion)
       return;
 
-#if 0
-    laser.num_damages--;
-    DrawLaser(0, DL_LASER_DISABLED);
-    laser.num_edges = 0;
-#endif
-
     Bang_MM(ELX, ELY);
 
     laser.dest_element = EL_EXPLODING_OPAQUE;
 
-#if 0
-    Bang_MM(ELX, ELY);
-    laser.num_damages--;
-    DrawLaser(0, DL_LASER_DISABLED);
-
-    laser.num_edges = 0;
-    Bang_MM(laser.start_edge.x, laser.start_edge.y);
-
-    if (Request("Bomb killed Mc Duffin! Play it again?",
-               REQ_ASK | REQ_STAY_CLOSED))
-    {
-      InitGame();
-    }
-    else
-    {
-      game_status = MAINMENU;
-      DrawMainMenu();
-    }
-#endif
-
     return;
   }
 
@@ -3839,182 +3787,6 @@ void MovePacMen(void)
   }
 }
 
-void GameWon_MM(void)
-{
-  int hi_pos;
-  boolean raise_level = FALSE;
-
-#if 0
-  if (local_player->MovPos)
-    return;
-
-  local_player->LevelSolved = FALSE;
-#endif
-
-  if (game_mm.energy_left)
-  {
-    if (setup.sound_loops)
-      PlaySoundExt(SND_SIRR, SOUND_MAX_VOLUME, SOUND_MAX_RIGHT,
-                  SND_CTRL_PLAY_LOOP);
-
-    while (game_mm.energy_left > 0)
-    {
-      if (!setup.sound_loops)
-       PlaySoundStereo(SND_SIRR, SOUND_MAX_RIGHT);
-
-      /*
-      if (game_mm.energy_left > 0 && !(game_mm.energy_left % 10))
-       RaiseScore_MM(native_mm_level.score[SC_ZEITBONUS]);
-      */
-
-      RaiseScore_MM(5);
-
-      game_mm.energy_left--;
-      if (game_mm.energy_left >= 0)
-      {
-#if 0
-       BlitBitmap(pix[PIX_DOOR], drawto,
-                  DOOR_GFX_PAGEX5 + XX_ENERGY, DOOR_GFX_PAGEY1 + YY_ENERGY,
-                  ENERGY_XSIZE, ENERGY_YSIZE - game_mm.energy_left,
-                  DX_ENERGY, DY_ENERGY);
-#endif
-       redraw_mask |= REDRAW_DOOR_1;
-      }
-
-      BackToFront();
-      Delay_WithScreenUpdates(10);
-    }
-
-    if (setup.sound_loops)
-      StopSound(SND_SIRR);
-  }
-  else if (native_mm_level.time == 0)          // level without time limit
-  {
-    if (setup.sound_loops)
-      PlaySoundExt(SND_SIRR, SOUND_MAX_VOLUME, SOUND_MAX_RIGHT,
-                  SND_CTRL_PLAY_LOOP);
-
-    while (TimePlayed < 999)
-    {
-      if (!setup.sound_loops)
-       PlaySoundStereo(SND_SIRR, SOUND_MAX_RIGHT);
-      if (TimePlayed < 999 && !(TimePlayed % 10))
-       RaiseScore_MM(native_mm_level.score[SC_TIME_BONUS]);
-      if (TimePlayed < 900 && !(TimePlayed % 10))
-       TimePlayed += 10;
-      else
-       TimePlayed++;
-
-      /*
-      DrawText(DX_TIME, DY_TIME, int2str(TimePlayed, 3), FONT_TEXT_2);
-      */
-
-      BackToFront();
-      Delay_WithScreenUpdates(10);
-    }
-
-    if (setup.sound_loops)
-      StopSound(SND_SIRR);
-  }
-
-  CloseDoor(DOOR_CLOSE_1);
-
-  Request("Level solved!", REQ_CONFIRM);
-
-  if (level_nr == leveldir_current->handicap_level)
-  {
-    leveldir_current->handicap_level++;
-    SaveLevelSetup_SeriesInfo();
-  }
-
-  if (level_editor_test_game)
-    game_mm.score = -1;                // no highscore when playing from editor
-  else if (level_nr < leveldir_current->last_level)
-    raise_level = TRUE;                // advance to next level
-
-  if ((hi_pos = NewHiScore_MM()) >= 0)
-  {
-    game_status = HALLOFFAME;
-
-    // DrawHallOfFame(hi_pos);
-
-    if (raise_level)
-      level_nr++;
-  }
-  else
-  {
-    game_status = MAINMENU;
-
-    if (raise_level)
-      level_nr++;
-
-    // DrawMainMenu();
-  }
-
-  BackToFront();
-}
-
-int NewHiScore_MM(void)
-{
-  int k, l;
-  int position = -1;
-
-  // LoadScore(level_nr);
-
-  if (strcmp(setup.player_name, EMPTY_PLAYER_NAME) == 0 ||
-      game_mm.score < highscore[MAX_SCORE_ENTRIES - 1].Score)
-    return -1;
-
-  for (k = 0; k < MAX_SCORE_ENTRIES; k++)
-  {
-    if (game_mm.score > highscore[k].Score)
-    {
-      // player has made it to the hall of fame
-
-      if (k < MAX_SCORE_ENTRIES - 1)
-      {
-       int m = MAX_SCORE_ENTRIES - 1;
-
-#ifdef ONE_PER_NAME
-       for (l = k; l < MAX_SCORE_ENTRIES; l++)
-         if (!strcmp(setup.player_name, highscore[l].Name))
-           m = l;
-       if (m == k)     // player's new highscore overwrites his old one
-         goto put_into_list;
-#endif
-
-       for (l = m; l>k; l--)
-       {
-         strcpy(highscore[l].Name, highscore[l - 1].Name);
-         highscore[l].Score = highscore[l - 1].Score;
-       }
-      }
-
-#ifdef ONE_PER_NAME
-      put_into_list:
-#endif
-      strncpy(highscore[k].Name, setup.player_name, MAX_PLAYER_NAME_LEN);
-      highscore[k].Name[MAX_PLAYER_NAME_LEN] = '\0';
-      highscore[k].Score = game_mm.score;
-      position = k;
-
-      break;
-    }
-
-#ifdef ONE_PER_NAME
-    else if (!strncmp(setup.player_name, highscore[k].Name,
-                     MAX_PLAYER_NAME_LEN))
-      break;   // player already there with a higher score
-#endif
-
-  }
-
-  // if (position >= 0)
-  //   SaveScore(level_nr);
-
-  return position;
-}
-
 static void InitMovingField_MM(int x, int y, int direction)
 {
   int newx = x + (direction == MV_LEFT ? -1 : direction == MV_RIGHT ? +1 : 0);
index f01f3fe..b223c73 100644 (file)
@@ -15,9 +15,6 @@
 #include "main_mm.h"
 
 
-void GameWon_MM(void);
-int NewHiScore_MM(void);
-
 void TurnRound(int, int);
 
 void PlaySoundLevel(int, int, int);
index 1beb507..af5b832 100644 (file)
 #define LEVEL_SCORE_ELEMENTS   16      // level elements with score
 
 
-struct HiScore_MM
-{
-  char Name[MAX_PLAYER_NAME_LEN + 1];
-  int Score;
-};
-
 extern DrawBuffer      *drawto_field;
 
 extern int             game_status;
@@ -225,7 +219,6 @@ extern int          SBY_Upper, SBY_Lower;
 extern int             TimeFrames, TimePlayed, TimeLeft;
 
 extern struct LevelInfo_MM     native_mm_level;
-extern struct HiScore_MM       highscore[];
 extern struct GameInfo_MM      game_mm;
 extern struct LaserInfo                laser;
 
@@ -1112,22 +1105,6 @@ extern int               num_element_info;
 #define GAME_OVER_OVERLOADED   2
 #define GAME_OVER_BOMB         3
 
-// values for game_status
-#define EXITGAME               0
-#define MAINMENU               1
-#define PLAYING                        2
-#define LEVELED                        3
-#define HELPSCREEN             4
-#define CHOOSELEVEL            5
-#define TYPENAME               6
-#define HALLOFFAME             7
-#define SETUP                  8
-
-// return values for GameActions
-#define ACT_GO_ON              0
-#define ACT_GAME_OVER          1
-#define ACT_NEW_GAME           2
-
 // values for color_status
 #define STATIC_COLORS          0
 #define DYNAMIC_COLORS         1
index e71fb1d..21ad081 100644 (file)
@@ -946,7 +946,7 @@ static void DrawTileCursor_Xsn(int draw_target)
     debug = TRUE;
     active = FALSE;
 
-    DelayReached(&check_delay, 0);
+    ResetDelayCounter(&check_delay);
 
     setup.debug.xsn_mode = (debug_value > 0);
     tile_cursor.xsn_debug = FALSE;
@@ -1000,7 +1000,7 @@ static void DrawTileCursor_Xsn(int draw_target)
                         (XSN_START_DELAY + XSN_RND(XSN_START_DELAY)) * 1000);
     started = FALSE;
 
-    DelayReached(&start_delay, 0);
+    ResetDelayCounter(&start_delay);
 
     reinitialize = TRUE;
   }
@@ -1079,9 +1079,9 @@ static void DrawTileCursor_Xsn(int draw_target)
     growth_delay_value = XSN_GROWTH_DELAY * 1000;
     change_delay_value = XSN_CHANGE_DELAY * 1000;
 
-    DelayReached(&growth_delay, 0);
-    DelayReached(&update_delay, 0);
-    DelayReached(&change_delay, 0);
+    ResetDelayCounter(&growth_delay);
+    ResetDelayCounter(&update_delay);
+    ResetDelayCounter(&change_delay);
 
     started = TRUE;
   }
index 210594f..2d6b750 100644 (file)
@@ -4664,7 +4664,7 @@ void InitElementPropertiesEngine(int engine_version)
                                                  i == EL_BLACK_ORB));
 
     // ---------- COULD_MOVE_INTO_ACID ----------------------------------------
-    SET_PROPERTY(i, EP_COULD_MOVE_INTO_ACID, (ELEM_IS_PLAYER(i) ||
+    SET_PROPERTY(i, EP_COULD_MOVE_INTO_ACID, (IS_PLAYER_ELEMENT(i) ||
                                              CAN_MOVE(i) ||
                                              IS_CUSTOM_ELEMENT(i)));
 
@@ -4903,7 +4903,10 @@ static void InitGlobal(void)
   global.autoplay_leveldir = NULL;
   global.patchtapes_leveldir = NULL;
   global.convert_leveldir = NULL;
-  global.create_images_dir = NULL;
+  global.dumplevel_leveldir = NULL;
+  global.dumptape_leveldir = NULL;
+  global.create_sketch_images_dir = NULL;
+  global.create_collect_images_dir = NULL;
 
   global.frames_per_second = 0;
   global.show_frames_per_second = FALSE;
@@ -5015,39 +5018,70 @@ static void Execute_Command(char *command)
   {
     char *filename = &command[11];
 
-    if (!fileExists(filename))
+    if (fileExists(filename))
+    {
+      LoadLevelFromFilename(&level, filename);
+      DumpLevel(&level);
+
+      exit(0);
+    }
+
+    char *leveldir = getStringCopy(filename);  // read command parameters
+    char *level_nr = strchr(leveldir, ' ');
+
+    if (level_nr == NULL)
       Fail("cannot open file '%s'", filename);
 
-    LoadLevelFromFilename(&level, filename);
-    DumpLevel(&level);
+    *level_nr++ = '\0';
 
-    exit(0);
+    global.dumplevel_leveldir = leveldir;
+    global.dumplevel_level_nr = atoi(level_nr);
+
+    program.headless = TRUE;
   }
   else if (strPrefix(command, "dump tape "))
   {
     char *filename = &command[10];
 
-    if (!fileExists(filename))
+    if (fileExists(filename))
+    {
+      LoadTapeFromFilename(filename);
+      DumpTape(&tape);
+
+      exit(0);
+    }
+
+    char *leveldir = getStringCopy(filename);  // read command parameters
+    char *level_nr = strchr(leveldir, ' ');
+
+    if (level_nr == NULL)
       Fail("cannot open file '%s'", filename);
 
-    LoadTapeFromFilename(filename);
-    DumpTape(&tape);
+    *level_nr++ = '\0';
 
-    exit(0);
+    global.dumptape_leveldir = leveldir;
+    global.dumptape_level_nr = atoi(level_nr);
+
+    program.headless = TRUE;
   }
   else if (strPrefix(command, "autoplay ") ||
           strPrefix(command, "autoffwd ") ||
           strPrefix(command, "autowarp ") ||
           strPrefix(command, "autotest ") ||
+          strPrefix(command, "autosave ") ||
+          strPrefix(command, "autoupload ") ||
           strPrefix(command, "autofix "))
   {
-    char *str_ptr = getStringCopy(&command[8]);        // read command parameters
+    char *arg_ptr = strchr(command, ' ');
+    char *str_ptr = getStringCopy(arg_ptr);    // read command parameters
 
     global.autoplay_mode =
       (strPrefix(command, "autoplay") ? AUTOPLAY_MODE_PLAY :
        strPrefix(command, "autoffwd") ? AUTOPLAY_MODE_FFWD :
        strPrefix(command, "autowarp") ? AUTOPLAY_MODE_WARP :
        strPrefix(command, "autotest") ? AUTOPLAY_MODE_TEST :
+       strPrefix(command, "autosave") ? AUTOPLAY_MODE_SAVE :
+       strPrefix(command, "autoupload") ? AUTOPLAY_MODE_UPLOAD :
        strPrefix(command, "autofix")  ? AUTOPLAY_MODE_FIX :
        AUTOPLAY_MODE_NONE);
 
@@ -5161,13 +5195,21 @@ static void Execute_Command(char *command)
 
     program.headless = TRUE;
   }
-  else if (strPrefix(command, "create images "))
+  else if (strPrefix(command, "create sketch images "))
   {
-    global.create_images_dir = getStringCopy(&command[14]);
+    global.create_sketch_images_dir = getStringCopy(&command[21]);
 
-    if (access(global.create_images_dir, W_OK) != 0)
+    if (access(global.create_sketch_images_dir, W_OK) != 0)
       Fail("image target directory '%s' not found or not writable",
-          global.create_images_dir);
+          global.create_sketch_images_dir);
+  }
+  else if (strPrefix(command, "create collect image "))
+  {
+    global.create_collect_images_dir = getStringCopy(&command[21]);
+
+    if (access(global.create_collect_images_dir, W_OK) != 0)
+      Fail("image target directory '%s' not found or not writable",
+          global.create_collect_images_dir);
   }
   else if (strPrefix(command, "create CE image "))
   {
@@ -5190,7 +5232,6 @@ static void InitSetup(void)
   LoadUserSetup();                             // global user number
 
   LoadSetup();                                 // global setup info
-  LoadSetup_AutoSetup();                       // global auto setup info
 
   // set some options from setup file
 
@@ -5488,7 +5529,7 @@ static void InitGfx(void)
 
   DrawProgramInfo();
 
-  DrawInitText("Loading graphics", 120, FC_GREEN);
+  DrawInitTextHead("Loading graphics");
 
   // initialize settings for busy animation with default values
   int parameter[NUM_GFX_ARGS];
@@ -6154,14 +6195,15 @@ void OpenAll(void)
 
   InitGlobal();                        // initialize some global variables
 
+  InitRND(NEW_RANDOMIZE);
+  InitSimpleRandom(NEW_RANDOMIZE);
+
   print_timestamp_time("[init global stuff]");
 
   InitSetup();
 
   print_timestamp_time("[init setup/config stuff (1)]");
 
-  InitScoresInfo();
-
   if (options.execute_command)
     Execute_Command(options.execute_command);
 
@@ -6191,9 +6233,6 @@ void OpenAll(void)
   InitMixer();
   print_timestamp_time("[init setup/config stuff (6)]");
 
-  InitRND(NEW_RANDOMIZE);
-  InitSimpleRandom(NEW_RANDOMIZE);
-
   InitJoysticks();
 
   print_timestamp_time("[init setup/config stuff]");
@@ -6258,11 +6297,26 @@ void OpenAll(void)
     ConvertLevels();
     return;
   }
-  else if (global.create_images_dir)
+  else if (global.dumplevel_leveldir)
+  {
+    DumpLevels();
+    return;
+  }
+  else if (global.dumptape_leveldir)
+  {
+    DumpTapes();
+    return;
+  }
+  else if (global.create_sketch_images_dir)
   {
     CreateLevelSketchImages();
     return;
   }
+  else if (global.create_collect_images_dir)
+  {
+    CreateCollectElementImages();
+    return;
+  }
 
   InitNetworkServer();
 
@@ -6276,6 +6330,9 @@ void OpenAll(void)
 
   print_timestamp_done("OpenAll");
 
+  if (setup.ask_for_remaining_tapes)
+    setup.ask_for_uploading_tapes = TRUE;
+
   DrawMainMenu();
 
 #if 0
@@ -6297,8 +6354,44 @@ void OpenAll(void)
 #endif
 }
 
+static boolean WaitForApiThreads(void)
+{
+  unsigned int thread_delay = 0;
+  unsigned int thread_delay_value = 10000;
+
+  if (program.api_thread_count == 0)
+    return TRUE;
+
+  // deactivate global animations (not accessible in game state "loading")
+  setup.toons = FALSE;
+
+  // set game state to "loading" to be able to show busy animation
+  SetGameStatus(GAME_MODE_LOADING);
+
+  ResetDelayCounter(&thread_delay);
+
+  // wait for threads to finish (and fail on timeout)
+  while (program.api_thread_count > 0)
+  {
+    if (DelayReached(&thread_delay, thread_delay_value))
+    {
+      Error("failed waiting for threads - TIMEOUT");
+
+      return FALSE;
+    }
+
+    UPDATE_BUSY_STATE();
+
+    Delay(20);
+  }
+
+  return TRUE;
+}
+
 void CloseAllAndExit(int exit_value)
 {
+  WaitForApiThreads();
+
   StopSounds();
   FreeAllSounds();
   FreeAllMusic();
index 26f3116..246655f 100644 (file)
@@ -22,6 +22,8 @@ SRCS =        system.c        \
        image.c         \
        random.c        \
        hash.c          \
+       http.c          \
+       base64.c        \
        setup.c         \
        misc.c          \
        sdl.c           \
@@ -39,6 +41,8 @@ OBJS =        system.o        \
        image.o         \
        random.o        \
        hash.o          \
+       http.o          \
+       base64.o        \
        setup.o         \
        misc.o          \
        sdl.o           \
diff --git a/src/libgame/base64.c b/src/libgame/base64.c
new file mode 100644 (file)
index 0000000..485f81b
--- /dev/null
@@ -0,0 +1,194 @@
+// ============================================================================
+// Artsoft Retro-Game Library
+// ----------------------------------------------------------------------------
+// (c) 1995-2021 by Artsoft Entertainment
+//                         Holger Schemel
+//                 info@artsoft.org
+//                 https://www.artsoft.org/
+// ----------------------------------------------------------------------------
+// base64.c
+// ============================================================================
+
+/*
+
+  https://github.com/superwills/NibbleAndAHalf
+  base64.h -- Fast base64 encoding and decoding.
+  version 1.0.0, April 17, 2013 143a
+
+  Copyright (C) 2013 William Sherif
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  William Sherif
+  will.sherif@gmail.com
+
+  YWxsIHlvdXIgYmFzZSBhcmUgYmVsb25nIHRvIHVz
+
+*/
+
+// ----------------------------------------------------------------------------
+// Base64 encoder/decoder code was altered for integration in Rocks'n'Diamonds
+// ----------------------------------------------------------------------------
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "base64.h"
+
+
+const static char *b64encode =
+  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+int base64_encoded_size(int unencoded_size)
+{
+  int mod = unencoded_size % 3;
+  int pad = (mod > 0 ? 3 - mod : 0);
+
+  return 4 * (unencoded_size + pad) / 3 + 1;
+}
+
+void base64_encode(char *encoded_data,
+                  const void *unencoded_ptr, int unencoded_size)
+{
+  const byte *unencoded_data = (const byte *)unencoded_ptr;
+  char *ptr = encoded_data;
+  int i;
+
+  int mod = unencoded_size % 3;
+  int pad = (mod > 0 ? 3 - mod : 0);
+
+  for (i = 0; i <= unencoded_size - 3; i += 3)
+  {
+    byte byte0 = unencoded_data[i];
+    byte byte1 = unencoded_data[i + 1];
+    byte byte2 = unencoded_data[i + 2];
+
+    *ptr++ = b64encode[byte0 >> 2];
+    *ptr++ = b64encode[((byte0 & 0x03) << 4) + (byte1 >> 4)];
+    *ptr++ = b64encode[((byte1 & 0x0f) << 2) + (byte2 >> 6)];
+    *ptr++ = b64encode[byte2 & 0x3f];
+  }
+
+  if (pad == 1)
+  {
+    byte byte0 = unencoded_data[i];
+    byte byte1 = unencoded_data[i + 1];
+
+    *ptr++ = b64encode[byte0 >> 2];
+    *ptr++ = b64encode[((byte0 & 0x03) << 4) + (byte1 >> 4)];
+    *ptr++ = b64encode[((byte1 & 0x0f) << 2)];
+    *ptr++ = '=';
+  }
+  else if (pad == 2)
+  {
+    byte byte0 = unencoded_data[i];
+
+    *ptr++ = b64encode[byte0 >> 2];
+    *ptr++ = b64encode[(byte0 & 0x03) << 4];
+    *ptr++ = '=';
+    *ptr++ = '=';
+  }
+
+  *ptr++= '\0';
+}
+
+const static byte b64decode[] =
+{
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      //   0
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      //  16
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 62,  0,  0,  0, 63,      //  32
+  52, 53, 54, 55, 56, 57, 58, 59, 60, 61,  0,  0,  0,  0,  0,  0,      //  48
+
+   0,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,      //  64
+  15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,  0,  0,  0,  0,  0,      //  80
+   0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,      //  96
+  41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,  0,  0,  0,  0,  0,      // 112
+
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      // 128
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      // 144
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      // 160
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      // 176
+
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      // 192
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      // 208
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      // 224
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,      // 240
+};
+
+int base64_decoded_size(const char *encoded_data)
+{
+  int encoded_size = strlen(encoded_data);
+
+  if (encoded_size < 2)
+    return 0;
+
+  int pad = 0;
+
+  if (encoded_data[encoded_size - 1] == '=')
+    pad++;
+  if (encoded_data[encoded_size - 2] == '=')
+    pad++;
+
+  return 3 * encoded_size / 4 - pad;
+}
+
+void base64_decode(byte *decoded_data, const char *encoded_ptr)
+{
+  const byte *encoded_data = (const byte *)encoded_ptr;
+  byte *ptr = decoded_data;
+  int encoded_size = strlen(encoded_ptr);
+  int i;
+
+  if (encoded_size < 2)
+    return;
+
+  int pad = 0;
+
+  if (encoded_data[encoded_size - 1] == '=')
+    pad++;
+  if (encoded_data[encoded_size - 2] == '=')
+    pad++;
+
+  for (i = 0; i <= encoded_size - 4 - pad; i += 4)
+  {
+    byte byte0 = b64decode[encoded_data[i]];
+    byte byte1 = b64decode[encoded_data[i + 1]];
+    byte byte2 = b64decode[encoded_data[i + 2]];
+    byte byte3 = b64decode[encoded_data[i + 3]];
+
+    *ptr++ = (byte0 << 2) | (byte1 >> 4);
+    *ptr++ = (byte1 << 4) | (byte2 >> 2);
+    *ptr++ = (byte2 << 6) | (byte3);
+  }
+
+  if (pad == 1)
+  {
+    byte byte0 = b64decode[encoded_data[i]];
+    byte byte1 = b64decode[encoded_data[i + 1]];
+    byte byte2 = b64decode[encoded_data[i + 2]];
+
+    *ptr++ = (byte0 << 2) | (byte1 >> 4);
+    *ptr++ = (byte1 << 4) | (byte2 >> 2);
+  }
+  else if (pad == 2)
+  {
+    byte byte0 = b64decode[encoded_data[i]];
+    byte byte1 = b64decode[encoded_data[i + 1]];
+
+    *ptr++ = (byte0 << 2) | (byte1 >> 4);
+  }
+}
diff --git a/src/libgame/base64.h b/src/libgame/base64.h
new file mode 100644 (file)
index 0000000..3c76e6a
--- /dev/null
@@ -0,0 +1,24 @@
+// ============================================================================
+// Artsoft Retro-Game Library
+// ----------------------------------------------------------------------------
+// (c) 1995-2021 by Artsoft Entertainment
+//                         Holger Schemel
+//                 info@artsoft.org
+//                 https://www.artsoft.org/
+// ----------------------------------------------------------------------------
+// base64.h
+// ============================================================================
+
+#ifndef BASE64_H
+#define BASE64_H
+
+#include "system.h"
+
+
+int base64_encoded_size(int);
+int base64_decoded_size(const char *);
+
+void base64_encode(char *, const void *, int);
+void base64_decode(byte *, const char *);
+
+#endif
index 6c394d7..e150a08 100644 (file)
@@ -1934,7 +1934,7 @@ boolean HandleGadgets(int mx, int my, int button)
     if (gadget_pressed)                // gadget pressed the first time
     {
       // initialize delay counter
-      DelayReached(&pressed_delay, 0);
+      ResetDelayCounter(&pressed_delay);
 
       // start gadget delay with longer delay after first click on gadget
       pressed_delay_value = GADGET_FRAME_DELAY_FIRST;
diff --git a/src/libgame/http.c b/src/libgame/http.c
new file mode 100644 (file)
index 0000000..0064bc1
--- /dev/null
@@ -0,0 +1,423 @@
+// ============================================================================
+// Artsoft Retro-Game Library
+// ----------------------------------------------------------------------------
+// (c) 1995-2021 by Artsoft Entertainment
+//                         Holger Schemel
+//                 info@artsoft.org
+//                 https://www.artsoft.org/
+// ----------------------------------------------------------------------------
+// http.c
+// ============================================================================
+
+#include <sys/stat.h>
+
+#include "platform.h"
+
+#include "http.h"
+#include "misc.h"
+
+
+static char http_error[MAX_HTTP_ERROR_SIZE];
+
+static void SetHttpError(char *format, ...)
+{
+  va_list ap;
+
+  va_start(ap, format);
+  vsnprintf(http_error, MAX_HTTP_ERROR_SIZE, format, ap);
+  va_end(ap);
+}
+
+char *GetHttpError(void)
+{
+  return http_error;
+}
+
+void ConvertHttpRequestBodyToServerEncoding(struct HttpRequest *request)
+{
+  char *body_utf8 = getUTF8FromLatin1(request->body);
+
+  strncpy(request->body, body_utf8, MAX_HTTP_BODY_SIZE);
+  request->body[MAX_HTTP_BODY_SIZE] = '\0';
+
+  checked_free(body_utf8);
+}
+
+void ConvertHttpResponseBodyToClientEncoding(struct HttpResponse *response)
+{
+  char *body_latin1 = getLatin1FromUTF8(response->body);
+
+  strncpy(response->body, body_latin1, MAX_HTTP_BODY_SIZE);
+  response->body[MAX_HTTP_BODY_SIZE] = '\0';
+
+  response->body_size = strlen(response->body);
+
+  checked_free(body_latin1);
+}
+
+static void SetHttpResponseToDefaults(struct HttpResponse *response)
+{
+  response->head[0] = '\0';
+  response->body[0] = '\0';
+  response->body_size = 0;
+
+  response->status_code = 0;
+  response->status_text[0] = '\0';
+}
+
+struct HttpResponse *GetHttpResponseFromBuffer(void *buffer, int size)
+{
+  if (size > MAX_HTTP_BODY_SIZE)
+    return NULL;
+
+  struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
+
+  SetHttpResponseToDefaults(response);
+
+  strncpy(response->body, buffer, MAX_HTTP_BODY_SIZE);
+  response->body[MAX_HTTP_BODY_SIZE] = '\0';
+  response->body_size = MIN(size, MAX_HTTP_BODY_SIZE);
+
+  return response;
+}
+
+static boolean SetHTTPResponseCode(struct HttpResponse *response, char *buffer)
+{
+  char *prefix = "HTTP/1.1 ";
+  char *prefix_start = strstr(buffer, prefix);
+
+  if (prefix_start == NULL)
+    return FALSE;
+
+  char *status_code_start = prefix_start + strlen(prefix);
+  char *status_code_end = strstr(status_code_start, " ");
+
+  if (status_code_end == NULL)
+    return FALSE;
+
+  int status_code_size = status_code_end - status_code_start;
+
+  if (status_code_size != 3)   // status code must have three digits
+    return FALSE;
+
+  char status_code[status_code_size + 1];
+
+  strncpy(status_code, status_code_start, status_code_size);
+  status_code[status_code_size] = '\0';
+
+  response->status_code = atoi(status_code);
+
+  char *status_text_start = status_code_end + 1;
+  char *status_text_end = strstr(status_text_start, "\r\n");
+
+  if (status_text_end == NULL)
+    return FALSE;
+
+  int status_text_size = status_text_end - status_text_start;
+
+  if (status_text_size > MAX_HTTP_ERROR_SIZE)
+    return FALSE;
+
+  strncpy(response->status_text, status_text_start, status_text_size);
+  response->status_text[status_text_size] = '\0';
+
+  return TRUE;
+}
+
+static boolean SetHTTPResponseHead(struct HttpResponse *response, char *buffer)
+{
+  char *separator = "\r\n\r\n";
+  char *separator_start = strstr(buffer, separator);
+
+  if (separator_start == NULL)
+    return FALSE;
+
+  int head_size = separator_start - buffer;
+
+  if (head_size > MAX_HTTP_HEAD_SIZE)
+    return FALSE;
+
+  strncpy(response->head, buffer, head_size);
+  response->head[head_size] = '\0';
+
+  return TRUE;
+}
+
+static boolean SetHTTPResponseBody(struct HttpResponse *response, char *buffer,
+                                  int buffer_size)
+{
+  char *separator = "\r\n\r\n";
+  char *separator_start = strstr(buffer, separator);
+
+  if (separator_start == NULL)
+    return FALSE;
+
+  int separator_size = strlen(separator);
+  int full_head_size = separator_start + separator_size - buffer;
+  int body_size = buffer_size - full_head_size;
+
+  if (body_size > MAX_HTTP_BODY_SIZE)
+    return FALSE;
+
+  memcpy(response->body, buffer + full_head_size, body_size);
+  response->body[body_size] = '\0';
+  response->body_size = body_size;
+
+  return TRUE;
+}
+
+static int GetHttpResponse(TCPsocket socket, char *buffer, int max_buffer_size)
+{
+  char *buffer_ptr = buffer;
+  int buffer_left = max_buffer_size;
+  int buffer_size = 0;
+  int response_size = 0;
+
+  while (1)
+  {
+    // read as many bytes to the buffer as possible
+    int bytes = SDLNet_TCP_Recv(socket, buffer_ptr, buffer_left);
+
+    if (bytes <= 0)
+    {
+      SetHttpError("receiving response from server failed");
+
+      return -1;
+    }
+
+    buffer_ptr += bytes;
+    buffer_size += bytes;
+    buffer_left -= bytes;
+
+    // check if response size was already determined
+    if (response_size > 0)
+    {
+      // check if response data was completely received
+      if (buffer_size >= response_size)
+       break;
+
+      // continue reading response body from server
+      continue;
+    }
+
+    char *separator = "\r\n\r\n";
+    char *separator_start = strstr(buffer, separator);
+    int separator_size = strlen(separator);
+
+    // check if response header was completely received
+    if (separator_start == NULL)
+    {
+      // continue reading response header from server
+      continue;
+    }
+
+    char *content_length = "Content-Length: ";
+    char *content_length_start = strstr(buffer, content_length);
+    int head_size = separator_start - buffer;
+
+    // check if response header contains content length header
+    if (content_length_start == NULL ||
+       content_length_start >= buffer + head_size)
+    {
+      SetHttpError("receiving 'Content-Length' header from server failed");
+
+      return -1;
+    }
+
+    char *content_length_value = content_length_start + strlen(content_length);
+    char *content_length_end = strstr(content_length_value, "\r\n");
+
+    // check if content length header has line termination
+    if (content_length_end == NULL)
+    {
+      SetHttpError("receiving 'Content-Length' value from server failed");
+
+      return -1;
+    }
+
+    int value_len = content_length_end - content_length_value;
+    int max_value_len = 10;
+
+    // check if content length header has valid size
+    if (value_len > max_value_len)
+    {
+      SetHttpError("received invalid 'Content-Length' value from server");
+
+      return -1;
+    }
+
+    char value_str[value_len + 1];
+
+    strncpy(value_str, content_length_value, value_len);
+    value_str[value_len] = '\0';
+
+    int body_size = atoi(value_str);
+
+    response_size = head_size + separator_size + body_size;
+
+    // check if response data was completely received
+    if (buffer_size >= response_size)
+      break;
+  }
+
+  return buffer_size;
+}
+
+static boolean DoHttpRequestExt(struct HttpRequest *request,
+                               struct HttpResponse *response,
+                               char *send_buffer,
+                               char *recv_buffer,
+                               int max_http_buffer_size,
+                               SDLNet_SocketSet *socket_set,
+                               TCPsocket *socket)
+{
+  IPaddress ip;
+  int server_host;
+
+  SetHttpResponseToDefaults(response);
+
+  *socket_set = SDLNet_AllocSocketSet(1);
+
+  if (*socket_set == NULL)
+  {
+    SetHttpError("cannot allocate socket set");
+
+    return FALSE;
+  }
+
+  SDLNet_ResolveHost(&ip, request->hostname, request->port);
+
+  if (ip.host == INADDR_NONE)
+  {
+    SetHttpError("cannot resolve hostname '%s'", request->hostname);
+
+    return FALSE;
+  }
+
+  server_host = SDLNet_Read32(&ip.host);
+
+  Debug("network:http", "trying to connect to server at %d.%d.%d.%d ...",
+        (server_host >> 24) & 0xff,
+        (server_host >> 16) & 0xff,
+        (server_host >>  8) & 0xff,
+        (server_host >>  0) & 0xff);
+
+  *socket = SDLNet_TCP_Open(&ip);
+
+  if (*socket == NULL)
+  {
+    SetHttpError("cannot connect to host '%s': %s", request->hostname,
+                SDLNet_GetError());
+
+    return FALSE;
+  }
+
+  if (SDLNet_TCP_AddSocket(*socket_set, *socket) == -1)
+  {
+    SetHttpError("cannot add socket to socket set");
+
+    return FALSE;
+  }
+
+  Debug("network:http", "successfully connected to server");
+
+  snprintf(request->head, MAX_HTTP_HEAD_SIZE,
+          "%s %s HTTP/1.1\r\n"
+          "Host: %s\r\n"
+          "X-Requested-With: XMLHttpRequest\r\n"
+          "Content-Type: application/json\r\n"
+          "Connection: close\r\n"
+          "Content-Length: %d\r\n",
+          request->method,
+          request->uri,
+          request->hostname,
+          (int)strlen(request->body));
+
+  snprintf(send_buffer, max_http_buffer_size,
+          "%s\r\n%s", request->head, request->body);
+
+  Debug("network:http", "client request:\n--- snip ---\n%s\n--- snip ---",
+       send_buffer);
+
+  int send_bytes = SDLNet_TCP_Send(*socket, send_buffer, strlen(send_buffer));
+
+  if (send_bytes != strlen(send_buffer))
+  {
+    SetHttpError("sending request to server failed");
+
+    return FALSE;
+  }
+
+  int recv_bytes = GetHttpResponse(*socket, recv_buffer, max_http_buffer_size);
+
+  if (recv_bytes <= 0)
+  {
+    // HTTP error already set in GetHttpResponse()
+
+    return FALSE;
+  }
+
+  recv_buffer[recv_bytes] = '\0';
+
+  Debug("network:http", "server response:\n--- snip ---\n%s\n--- snip ---",
+       recv_buffer);
+
+  if (!SetHTTPResponseCode(response, recv_buffer))
+  {
+    SetHttpError("malformed HTTP response");
+
+    return FALSE;
+  }
+
+  if (!SetHTTPResponseHead(response, recv_buffer))
+  {
+    SetHttpError("invalid HTTP response header");
+
+    return FALSE;
+  }
+
+  if (!SetHTTPResponseBody(response, recv_buffer, recv_bytes))
+  {
+    SetHttpError("invalid HTTP response body");
+
+    return FALSE;
+  }
+
+  Debug("network:http", "server response: %d %s",
+       response->status_code,
+       response->status_text);
+
+  return TRUE;
+}
+
+boolean DoHttpRequest(struct HttpRequest *request,
+                     struct HttpResponse *response)
+{
+  int max_http_buffer_size = MAX_HTTP_HEAD_SIZE + MAX_HTTP_BODY_SIZE;
+  char *send_buffer = checked_malloc(max_http_buffer_size + 1);
+  char *recv_buffer = checked_malloc(max_http_buffer_size + 1);
+  SDLNet_SocketSet socket_set = NULL;
+  TCPsocket socket = NULL;
+
+  boolean success = DoHttpRequestExt(request, response,
+                                    send_buffer, recv_buffer,
+                                    max_http_buffer_size,
+                                    &socket_set, &socket);
+  if (socket_set != NULL)
+  {
+    if (socket != NULL)
+    {
+      SDLNet_TCP_DelSocket(socket_set, socket);
+      SDLNet_TCP_Close(socket);
+    }
+
+    SDLNet_FreeSocketSet(socket_set);
+  }
+
+  checked_free(send_buffer);
+  checked_free(recv_buffer);
+
+  runtime.use_api_server = success;
+
+  return success;
+}
diff --git a/src/libgame/http.h b/src/libgame/http.h
new file mode 100644 (file)
index 0000000..2523bc7
--- /dev/null
@@ -0,0 +1,52 @@
+// ============================================================================
+// Artsoft Retro-Game Library
+// ----------------------------------------------------------------------------
+// (c) 1995-2021 by Artsoft Entertainment
+//                         Holger Schemel
+//                 info@artsoft.org
+//                 https://www.artsoft.org/
+// ----------------------------------------------------------------------------
+// http.h
+// ============================================================================
+
+#ifndef HTTP_H
+#define HTTP_H
+
+#include "system.h"
+
+#define MAX_HTTP_HEAD_SIZE             4096
+#define MAX_HTTP_BODY_SIZE             1048576
+#define MAX_HTTP_ERROR_SIZE            1024
+
+#define HTTP_SUCCESS(c)                        ((c) >= 200 && (c) < 300)
+
+
+struct HttpRequest
+{
+  char head[MAX_HTTP_HEAD_SIZE + 1];
+  char body[MAX_HTTP_BODY_SIZE + 1];
+
+  char *hostname;
+  int port;
+  char *method;
+  char *uri;
+};
+
+struct HttpResponse
+{
+  char head[MAX_HTTP_HEAD_SIZE + 1];
+  char body[MAX_HTTP_BODY_SIZE + 1];
+  int body_size;
+
+  int status_code;
+  char status_text[MAX_HTTP_ERROR_SIZE + 1];
+};
+
+
+char *GetHttpError(void);
+void ConvertHttpRequestBodyToServerEncoding(struct HttpRequest *);
+void ConvertHttpResponseBodyToClientEncoding(struct HttpResponse *);
+struct HttpResponse *GetHttpResponseFromBuffer(void *, int);
+boolean DoHttpRequest(struct HttpRequest *, struct HttpResponse *);
+
+#endif
index 8827257..cf95a10 100644 (file)
 #include "system.h"
 
 
-// these bitmap pointers either point to allocated bitmaps or are NULL
+// bitmap array positions for various element sizes, if available
+//
+// for any loaded image, the "standard" size (which represents the 32x32 pixel
+// size for game elements) is always defined; other bitmap sizes may be NULL
+//
+// formats from 32x32 down to 1x1 are standard bitmap sizes for game elements
+// (used in the game, in the level editor, in the level preview etc.)
+//
+// "CUSTOM" sizes for game elements (like 64x64) may be additionally created;
+// all "OTHER" image sizes are stored if different from all other bitmap sizes,
+// which may be used "as is" by global animations (as the "standard" size used
+// normally may be wrong due to being scaled up or down to a different size if
+// the same image contains game elements in a non-standard size)
+
 #define IMG_BITMAP_32x32       0
 #define IMG_BITMAP_16x16       1
 #define IMG_BITMAP_8x8         2
 #define IMG_BITMAP_2x2         4
 #define IMG_BITMAP_1x1         5
 #define IMG_BITMAP_CUSTOM      6
+#define IMG_BITMAP_OTHER       7
 
-#define NUM_IMG_BITMAPS                7
+#define NUM_IMG_BITMAPS                8
 
-// this bitmap pointer points to one of the above bitmaps (do not free it)
-#define IMG_BITMAP_GAME                7
+// these bitmap pointers point to one of the above bitmaps (do not free them)
+#define IMG_BITMAP_PTR_GAME    8
+#define IMG_BITMAP_PTR_ORIGINAL        9
 
-#define NUM_IMG_BITMAP_POINTERS        8
+#define NUM_IMG_BITMAP_POINTERS        10
 
 // this bitmap pointer points to the bitmap with default image size
 #define IMG_BITMAP_STANDARD    IMG_BITMAP_32x32
index 135b378..2573a63 100644 (file)
@@ -26,6 +26,8 @@
 #include "image.h"
 #include "setup.h"
 #include "misc.h"
+#include "http.h"
+#include "base64.h"
 #include "zip/miniunz.h"
 
 #endif // LIBGAME_H
index 7a3afc6..faf6c01 100644 (file)
@@ -549,6 +549,40 @@ boolean getTokenValueFromString(char *string, char **token, char **value)
 }
 
 
+// ----------------------------------------------------------------------------
+// UUID functions
+// ----------------------------------------------------------------------------
+
+#define UUID_BYTES             16
+#define UUID_CHARS             (UUID_BYTES * 2)
+#define UUID_LENGTH            (UUID_CHARS + 4)
+
+char *getUUID(void)
+{
+  static char uuid[UUID_LENGTH + 1];
+  int data[UUID_BYTES];
+  int count = 0;
+  int i;
+
+  for (i = 0; i < UUID_BYTES; i++)
+    data[i] = GetSimpleRandom(256);
+
+  data[6] = 0x40 | (data[6] & 0x0f);
+  data[8] = 0x80 | (data[8] & 0x3f);
+
+  for (i = 0; i < UUID_BYTES; i++)
+  {
+    sprintf(&uuid[count], "%02x", data[i]);
+    count += 2;
+
+    if (i == 3 || i == 5 || i == 7 || i == 9)
+      strcat(&uuid[count++], "-");
+  }
+
+  return uuid;
+}
+
+
 // ----------------------------------------------------------------------------
 // counter functions
 // ----------------------------------------------------------------------------
@@ -1240,8 +1274,7 @@ void GetOptions(int argc, char *argv[],
                void (*print_usage_function)(void),
                void (*print_version_function)(void))
 {
-  char *ro_base_path = getProgramMainDataPath(argv[0], RO_BASE_PATH);
-  char *rw_base_path = getProgramMainDataPath(argv[0], RW_BASE_PATH);
+  char *base_path = getProgramMainDataPath(argv[0], BASE_PATH);
   char **argvplus = checked_calloc((argc + 1) * sizeof(char **));
   char **options_left = &argvplus[1];
 
@@ -1253,18 +1286,22 @@ void GetOptions(int argc, char *argv[],
   options.server_host = NULL;
   options.server_port = 0;
 
-  options.ro_base_directory = ro_base_path;
-  options.rw_base_directory = rw_base_path;
-  options.level_directory    = getPath2(ro_base_path, LEVELS_DIRECTORY);
-  options.graphics_directory = getPath2(ro_base_path, GRAPHICS_DIRECTORY);
-  options.sounds_directory   = getPath2(ro_base_path, SOUNDS_DIRECTORY);
-  options.music_directory    = getPath2(ro_base_path, MUSIC_DIRECTORY);
-  options.docs_directory     = getPath2(ro_base_path, DOCS_DIRECTORY);
-  options.conf_directory     = getPath2(ro_base_path, CONF_DIRECTORY);
+  options.base_directory = base_path;
+
+  options.level_directory    = getPath2(base_path, LEVELS_DIRECTORY);
+  options.graphics_directory = getPath2(base_path, GRAPHICS_DIRECTORY);
+  options.sounds_directory   = getPath2(base_path, SOUNDS_DIRECTORY);
+  options.music_directory    = getPath2(base_path, MUSIC_DIRECTORY);
+  options.docs_directory     = getPath2(base_path, DOCS_DIRECTORY);
+  options.conf_directory     = getPath2(base_path, CONF_DIRECTORY);
 
   options.execute_command = NULL;
+  options.tape_log_filename = NULL;
   options.special_flags = NULL;
   options.debug_mode = NULL;
+  options.player_name = NULL;
+  options.identifier = NULL;
+  options.level_nr = NULL;
 
   options.mytapes = FALSE;
   options.serveronly = FALSE;
@@ -1334,19 +1371,17 @@ void GetOptions(int argc, char *argv[],
       if (option_arg == NULL)
        FailWithHelp("option '%s' requires an argument", option_str);
 
-      // this should be extended to separate options for ro and rw data
-      options.ro_base_directory = ro_base_path = getStringCopy(option_arg);
-      options.rw_base_directory = rw_base_path = getStringCopy(option_arg);
+      options.base_directory = base_path = getStringCopy(option_arg);
       if (option_arg == next_option)
        options_left++;
 
       // adjust paths for sub-directories in base directory accordingly
-      options.level_directory    = getPath2(ro_base_path, LEVELS_DIRECTORY);
-      options.graphics_directory = getPath2(ro_base_path, GRAPHICS_DIRECTORY);
-      options.sounds_directory   = getPath2(ro_base_path, SOUNDS_DIRECTORY);
-      options.music_directory    = getPath2(ro_base_path, MUSIC_DIRECTORY);
-      options.docs_directory     = getPath2(ro_base_path, DOCS_DIRECTORY);
-      options.conf_directory     = getPath2(ro_base_path, CONF_DIRECTORY);
+      options.level_directory    = getPath2(base_path, LEVELS_DIRECTORY);
+      options.graphics_directory = getPath2(base_path, GRAPHICS_DIRECTORY);
+      options.sounds_directory   = getPath2(base_path, SOUNDS_DIRECTORY);
+      options.music_directory    = getPath2(base_path, MUSIC_DIRECTORY);
+      options.docs_directory     = getPath2(base_path, DOCS_DIRECTORY);
+      options.conf_directory     = getPath2(base_path, CONF_DIRECTORY);
     }
     else if (strncmp(option, "-levels", option_len) == 0)
     {
@@ -1404,6 +1439,33 @@ void GetOptions(int argc, char *argv[],
       if (option_arg != next_option)
        options.debug_mode = getStringCopy(option_arg);
     }
+    else if (strncmp(option, "-player-name", option_len) == 0)
+    {
+      if (option_arg == NULL)
+       FailWithHelp("option '%s' requires an argument", option_str);
+
+      options.player_name = getStringCopy(option_arg);
+      if (option_arg == next_option)
+       options_left++;
+    }
+    else if (strncmp(option, "-identifier", option_len) == 0)
+    {
+      if (option_arg == NULL)
+       FailWithHelp("option '%s' requires an argument", option_str);
+
+      options.identifier = getStringCopy(option_arg);
+      if (option_arg == next_option)
+       options_left++;
+    }
+    else if (strncmp(option, "-level-nr", option_len) == 0)
+    {
+      if (option_arg == NULL)
+       FailWithHelp("option '%s' requires an argument", option_str);
+
+      options.level_nr = getStringCopy(option_arg);
+      if (option_arg == next_option)
+       options_left++;
+    }
     else if (strncmp(option, "-verbose", option_len) == 0)
     {
       options.verbose = TRUE;
@@ -1431,6 +1493,15 @@ void GetOptions(int argc, char *argv[],
       // when doing batch processing, always enable verbose mode (warnings)
       options.verbose = TRUE;
     }
+    else if (strncmp(option, "-tape_logfile", option_len) == 0)
+    {
+      if (option_arg == NULL)
+       FailWithHelp("option '%s' requires an argument", option_str);
+
+      options.tape_log_filename = getStringCopy(option_arg);
+      if (option_arg == next_option)
+       options_left++;
+    }
 #if defined(PLATFORM_MACOSX)
     else if (strPrefix(option, "-psn"))
     {
@@ -1725,6 +1796,167 @@ void WriteUnusedBytesToFile(FILE *file, unsigned int bytes)
 }
 
 
+// ----------------------------------------------------------------------------
+// functions to convert between ISO-8859-1 and UTF-8
+// ----------------------------------------------------------------------------
+
+char *getUTF8FromLatin1(char *latin1)
+{
+  int max_utf8_size = 2 * strlen(latin1) + 1;
+  char *utf8 = checked_calloc(max_utf8_size);
+  unsigned char *src = (unsigned char *)latin1;
+  unsigned char *dst = (unsigned char *)utf8;
+
+  while (*src)
+  {
+    if (*src < 128)            // pure 7-bit ASCII
+    {
+      *dst++ = *src;
+    }
+    else if (*src >= 160)      // non-ASCII characters
+    {
+      *dst++ = 194 + (*src >= 192);
+      *dst++ = 128 + (*src & 63);
+    }
+    else                       // undefined in ISO-8859-1
+    {
+      *dst++ = '?';
+    }
+
+    src++;
+  }
+
+  // only use the smallest possible string buffer size
+  utf8 = checked_realloc(utf8, strlen(utf8) + 1);
+
+  return utf8;
+}
+
+char *getLatin1FromUTF8(char *utf8)
+{
+  int max_latin1_size = strlen(utf8) + 1;
+  char *latin1 = checked_calloc(max_latin1_size);
+  unsigned char *src = (unsigned char *)utf8;
+  unsigned char *dst = (unsigned char *)latin1;
+
+  while (*src)
+  {
+    if (*src < 128)                            // pure 7-bit ASCII
+    {
+      *dst++ = *src++;
+    }
+    else if (src[0] == 194 &&
+            src[1] >= 128 && src[1] < 192)     // non-ASCII characters
+    {
+      *dst++ = src[1];
+      src += 2;
+    }
+    else if (src[0] == 195 &&
+            src[1] >= 128 && src[1] < 192)     // non-ASCII characters
+    {
+      *dst++ = src[1] + 64;
+      src += 2;
+    }
+
+    // all other UTF-8 characters are undefined in ISO-8859-1
+
+    else if (src[0] >= 192 && src[0] < 224 &&
+            src[1] >= 128 && src[1] < 192)
+    {
+      *dst++ = '?';
+      src += 2;
+    }
+    else if (src[0] >= 224 && src[0] < 240 &&
+            src[1] >= 128 && src[1] < 192 &&
+            src[2] >= 128 && src[2] < 192)
+    {
+      *dst++ = '?';
+      src += 3;
+    }
+    else if (src[0] >= 240 && src[0] < 248 &&
+            src[1] >= 128 && src[1] < 192 &&
+            src[2] >= 128 && src[2] < 192 &&
+            src[3] >= 128 && src[3] < 192)
+    {
+      *dst++ = '?';
+      src += 4;
+    }
+    else if (src[0] >= 248 && src[0] < 252 &&
+            src[1] >= 128 && src[1] < 192 &&
+            src[2] >= 128 && src[2] < 192 &&
+            src[3] >= 128 && src[3] < 192 &&
+            src[4] >= 128 && src[4] < 192)
+    {
+      *dst++ = '?';
+      src += 5;
+    }
+    else if (src[0] >= 252 && src[0] < 254 &&
+            src[1] >= 128 && src[1] < 192 &&
+            src[2] >= 128 && src[2] < 192 &&
+            src[3] >= 128 && src[3] < 192 &&
+            src[4] >= 128 && src[4] < 192 &&
+            src[5] >= 128 && src[5] < 192)
+    {
+      *dst++ = '?';
+      src += 6;
+    }
+    else
+    {
+      *dst++ = '?';
+      src++;
+    }
+  }
+
+  // only use the smallest possible string buffer size
+  latin1 = checked_realloc(latin1, strlen(latin1) + 1);
+
+  return latin1;
+}
+
+
+// ----------------------------------------------------------------------------
+// functions for JSON handling
+// ----------------------------------------------------------------------------
+
+char *getEscapedJSON(char *s)
+{
+  int max_json_size = 2 * strlen(s) + 1;
+  char *json = checked_calloc(max_json_size);
+  unsigned char *src = (unsigned char *)s;
+  unsigned char *dst = (unsigned char *)json;
+  char *escaped[256] =
+  {
+    ['\b'] = "\\b",
+    ['\f'] = "\\f",
+    ['\n'] = "\\n",
+    ['\r'] = "\\r",
+    ['\t'] = "\\t",
+    ['\"'] = "\\\"",
+    ['\\'] = "\\\\",
+  };
+
+  while (*src)
+  {
+    if (escaped[*src] != NULL)
+    {
+      char *esc = escaped[*src++];
+
+      while (*esc)
+       *dst++ = *esc++;
+    }
+    else
+    {
+      *dst++ = *src++;
+    }
+  }
+
+  // only use the smallest possible string buffer size
+  json = checked_realloc(json, strlen(json) + 1);
+
+  return json;
+}
+
+
 // ----------------------------------------------------------------------------
 // functions to translate key identifiers between different format
 // ----------------------------------------------------------------------------
@@ -2554,6 +2786,22 @@ int copyFile(char *filename_from, char *filename_to)
   return 0;
 }
 
+boolean touchFile(char *filename)
+{
+  FILE *file;
+
+  if (!(file = fopen(filename, MODE_WRITE)))
+  {
+    Warn("cannot touch file '%s'", filename);
+
+    return FALSE;
+  }
+
+  fclose(file);
+
+  return TRUE;
+}
+
 
 // ----------------------------------------------------------------------------
 // functions for directory handling
@@ -3568,8 +3816,8 @@ void LoadArtworkConfig(struct ArtworkListInfo *artwork_info)
   char *filename_base = UNDEFINED_FILENAME, *filename_local;
   int i, j;
 
-  DrawInitText("Loading artwork config", 120, FC_GREEN);
-  DrawInitText(ARTWORKINFO_FILENAME(artwork_info->type), 150, FC_YELLOW);
+  DrawInitTextHead("Loading artwork config");
+  DrawInitTextItem(ARTWORKINFO_FILENAME(artwork_info->type));
 
   // always start with reliable default values
   for (i = 0; i < num_file_list_entries; i++)
@@ -3654,6 +3902,11 @@ static void replaceArtworkListEntry(struct ArtworkListInfo *artwork_info,
   char *basename = file_list_entry->filename;
   char *filename = getCustomArtworkFilename(basename, artwork_info->type);
 
+  // mark all images from non-default graphics directory as "redefined"
+  if (artwork_info->type == ARTWORK_TYPE_GRAPHICS &&
+      !strPrefix(filename, options.graphics_directory))
+    file_list_entry->redefined = TRUE;
+
   if (filename == NULL)
   {
     Warn("cannot find artwork file '%s'", basename);
@@ -3722,8 +3975,8 @@ static void replaceArtworkListEntry(struct ArtworkListInfo *artwork_info,
       return;
   }
 
-  DrawInitText(init_text[artwork_info->type], 120, FC_GREEN);
-  DrawInitText(basename, 150, FC_YELLOW);
+  DrawInitTextHead(init_text[artwork_info->type]);
+  DrawInitTextItem(basename);
 
   if ((*listnode = artwork_info->load_artwork(filename)) != NULL)
   {
index 66b310e..27ae0f3 100644 (file)
@@ -120,6 +120,8 @@ int log_2(unsigned int);
 
 boolean getTokenValueFromString(char *, char **, char **);
 
+char *getUUID(void);
+
 void InitCounter(void);
 unsigned int Counter(void);
 void Delay(unsigned int);
@@ -225,6 +227,10 @@ void WriteUnusedBytesToFile(FILE *, unsigned int);
 #define putFileChunkBE(f,s,x) putFileChunk(f,s,x,BYTE_ORDER_BIG_ENDIAN)
 #define putFileChunkLE(f,s,x) putFileChunk(f,s,x,BYTE_ORDER_LITTLE_ENDIAN)
 
+char *getUTF8FromLatin1(char *);
+char *getLatin1FromUTF8(char *);
+char *getEscapedJSON(char *);
+
 char *getKeyNameFromKey(Key);
 char *getX11KeyNameFromKey(Key);
 Key getKeyFromKeyName(char *);
@@ -252,6 +258,7 @@ int seekFile(File *, long, int);
 int getByteFromFile(File *);
 char *getStringFromFile(File *, char *, int);
 int copyFile(char *, char *);
+boolean touchFile(char *);
 
 Directory *openDirectory(char *);
 int closeDirectory(Directory *);
index 438e8de..ee7bff3 100644 (file)
@@ -38,7 +38,7 @@
 #if defined(AMIGA) || defined(__AMIGA) || defined(__amigados__)
 #define PLATFORM_AMIGA
 #undef  PLATFORM_STRING
-#define PLATFORM_STRING "AmigaOS"
+#define PLATFORM_STRING "Amiga"
 #endif
 
 #if defined(__BEOS__)
@@ -88,7 +88,7 @@
 #if defined(__APPLE__) && defined(__MACH__)
 #define PLATFORM_MACOSX
 #undef  PLATFORM_STRING
-#define PLATFORM_STRING "Mac OS X"
+#define PLATFORM_STRING "Mac"
 #endif
 
 #if defined(__NetBSD__)
index 32ae112..9ea1754 100644 (file)
@@ -88,6 +88,16 @@ static char *getLevelClassDescription(TreeInfo *ti)
     return "Unknown Level Class";
 }
 
+static char *getCacheDir(void)
+{
+  static char *cache_dir = NULL;
+
+  if (cache_dir == NULL)
+    cache_dir = getPath2(getMainUserGameDataDir(), CACHE_DIRECTORY);
+
+  return cache_dir;
+}
+
 static char *getScoreDir(char *level_subdir)
 {
   static char *score_dir = NULL;
@@ -95,13 +105,29 @@ static char *getScoreDir(char *level_subdir)
   char *score_subdir = SCORES_DIRECTORY;
 
   if (score_dir == NULL)
+    score_dir = getPath2(getMainUserGameDataDir(), score_subdir);
+
+  if (level_subdir != NULL)
   {
-    if (program.global_scores)
-      score_dir = getPath2(getCommonDataDir(),       score_subdir);
-    else
-      score_dir = getPath2(getMainUserGameDataDir(), score_subdir);
+    checked_free(score_level_dir);
+
+    score_level_dir = getPath2(score_dir, level_subdir);
+
+    return score_level_dir;
   }
 
+  return score_dir;
+}
+
+static char *getScoreCacheDir(char *level_subdir)
+{
+  static char *score_dir = NULL;
+  static char *score_level_dir = NULL;
+  char *score_subdir = SCORES_DIRECTORY;
+
+  if (score_dir == NULL)
+    score_dir = getPath2(getCacheDir(), score_subdir);
+
   if (level_subdir != NULL)
   {
     checked_free(score_level_dir);
@@ -114,6 +140,19 @@ static char *getScoreDir(char *level_subdir)
   return score_dir;
 }
 
+static char *getScoreTapeDir(char *level_subdir, int nr)
+{
+  static char *score_tape_dir = NULL;
+  char tape_subdir[MAX_FILENAME_LEN];
+
+  checked_free(score_tape_dir);
+
+  sprintf(tape_subdir, "%03d", nr);
+  score_tape_dir = getPath2(getScoreDir(level_subdir), tape_subdir);
+
+  return score_tape_dir;
+}
+
 static char *getUserSubdir(int nr)
 {
   static char user_subdir[16] = { 0 };
@@ -156,16 +195,6 @@ static char *getLevelSetupDir(char *level_subdir)
   return levelsetup_dir;
 }
 
-static char *getCacheDir(void)
-{
-  static char *cache_dir = NULL;
-
-  if (cache_dir == NULL)
-    cache_dir = getPath2(getMainUserGameDataDir(), CACHE_DIRECTORY);
-
-  return cache_dir;
-}
-
 static char *getNetworkDir(void)
 {
   static char *network_dir = NULL;
@@ -250,7 +279,7 @@ char *getNewUserLevelSubdir(void)
   return new_level_subdir;
 }
 
-static char *getTapeDir(char *level_subdir)
+char *getTapeDir(char *level_subdir)
 {
   static char *tape_dir = NULL;
   char *data_dir = getUserGameDataDir();
@@ -505,8 +534,8 @@ char *getProgramConfigFilename(char *command_filename)
     if (strSuffix(command_filename_1, ".exe"))
       command_filename_1[strlen(command_filename_1) - 4] = '\0';
 
-    char *ro_base_path = getProgramMainDataPath(command_filename, RO_BASE_PATH);
-    char *conf_directory = getPath2(ro_base_path, CONF_DIRECTORY);
+    char *base_path = getProgramMainDataPath(command_filename, BASE_PATH);
+    char *conf_directory = getPath2(base_path, CONF_DIRECTORY);
 
     char *command_basepath = getBasePath(command_filename);
     char *command_basename = getBaseNameNoSuffix(command_filename);
@@ -516,7 +545,7 @@ char *getProgramConfigFilename(char *command_filename)
     config_filename_2 = getStringCat2(command_filename_2, ".conf");
     config_filename_3 = getPath2(conf_directory, SETUP_FILENAME);
 
-    checked_free(ro_base_path);
+    checked_free(base_path);
     checked_free(conf_directory);
 
     checked_free(command_basepath);
@@ -553,7 +582,20 @@ char *getTapeFilename(int nr)
   return filename;
 }
 
-char *getSolutionTapeFilename(int nr)
+char *getTemporaryTapeFilename(void)
+{
+  static char *filename = NULL;
+  char basename[MAX_FILENAME_LEN];
+
+  checked_free(filename);
+
+  sprintf(basename, "tmp.%s", TAPEFILE_EXTENSION);
+  filename = getPath2(getTapeDir(NULL), basename);
+
+  return filename;
+}
+
+char *getDefaultSolutionTapeFilename(int nr)
 {
   static char *filename = NULL;
   char basename[MAX_FILENAME_LEN];
@@ -563,17 +605,32 @@ char *getSolutionTapeFilename(int nr)
   sprintf(basename, "%03d.%s", nr, TAPEFILE_EXTENSION);
   filename = getPath2(getSolutionTapeDir(), basename);
 
-  if (!fileExists(filename))
-  {
-    static char *filename_sln = NULL;
+  return filename;
+}
 
-    checked_free(filename_sln);
+char *getSokobanSolutionTapeFilename(int nr)
+{
+  static char *filename = NULL;
+  char basename[MAX_FILENAME_LEN];
 
-    sprintf(basename, "%03d.sln", nr);
-    filename_sln = getPath2(getSolutionTapeDir(), basename);
+  checked_free(filename);
 
-    if (fileExists(filename_sln))
-      return filename_sln;
+  sprintf(basename, "%03d.sln", nr);
+  filename = getPath2(getSolutionTapeDir(), basename);
+
+  return filename;
+}
+
+char *getSolutionTapeFilename(int nr)
+{
+  char *filename = getDefaultSolutionTapeFilename(nr);
+
+  if (!fileExists(filename))
+  {
+    char *filename2 = getSokobanSolutionTapeFilename(nr);
+
+    if (fileExists(filename2))
+      return filename2;
   }
 
   return filename;
@@ -594,6 +651,49 @@ char *getScoreFilename(int nr)
   return filename;
 }
 
+char *getScoreCacheFilename(int nr)
+{
+  static char *filename = NULL;
+  char basename[MAX_FILENAME_LEN];
+
+  checked_free(filename);
+
+  sprintf(basename, "%03d.%s", nr, SCOREFILE_EXTENSION);
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  filename = getPath2(getScoreCacheDir(levelset.identifier), basename);
+
+  return filename;
+}
+
+char *getScoreTapeBasename(char *name)
+{
+  static char basename[MAX_FILENAME_LEN];
+  char basename_raw[MAX_FILENAME_LEN];
+  char timestamp[20];
+
+  sprintf(timestamp, "%s", getCurrentTimestamp());
+  sprintf(basename_raw, "%s-%s", timestamp, name);
+  sprintf(basename, "%s-%08x", timestamp, get_hash_from_key(basename_raw));
+
+  return basename;
+}
+
+char *getScoreTapeFilename(char *basename_no_ext, int nr)
+{
+  static char *filename = NULL;
+  char basename[MAX_FILENAME_LEN];
+
+  checked_free(filename);
+
+  sprintf(basename, "%s.%s", basename_no_ext, TAPEFILE_EXTENSION);
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  filename = getPath2(getScoreTapeDir(levelset.identifier, nr), basename);
+
+  return filename;
+}
+
 char *getSetupFilename(void)
 {
   static char *filename = NULL;
@@ -1055,24 +1155,66 @@ char *getCustomMusicDirectory(void)
   return NULL;         // cannot find specified artwork file anywhere
 }
 
+void MarkTapeDirectoryUploadsAsComplete(char *level_subdir)
+{
+  char *filename = getPath2(getTapeDir(level_subdir), UPLOADED_FILENAME);
+
+  touchFile(filename);
+
+  checked_free(filename);
+}
+
+void MarkTapeDirectoryUploadsAsIncomplete(char *level_subdir)
+{
+  char *filename = getPath2(getTapeDir(level_subdir), UPLOADED_FILENAME);
+
+  unlink(filename);
+
+  checked_free(filename);
+}
+
+boolean CheckTapeDirectoryUploadsComplete(char *level_subdir)
+{
+  char *filename = getPath2(getTapeDir(level_subdir), UPLOADED_FILENAME);
+  boolean success = fileExists(filename);
+
+  checked_free(filename);
+
+  return success;
+}
+
 void InitTapeDirectory(char *level_subdir)
 {
+  boolean new_tape_dir = !directoryExists(getTapeDir(level_subdir));
+
   createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
   createDirectory(getTapeDir(NULL), "main tape", PERMS_PRIVATE);
   createDirectory(getTapeDir(level_subdir), "level tape", PERMS_PRIVATE);
+
+  if (new_tape_dir)
+    MarkTapeDirectoryUploadsAsComplete(level_subdir);
 }
 
 void InitScoreDirectory(char *level_subdir)
 {
-  int permissions = (program.global_scores ? PERMS_PUBLIC : PERMS_PRIVATE);
+  createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
+  createDirectory(getScoreDir(NULL), "main score", PERMS_PRIVATE);
+  createDirectory(getScoreDir(level_subdir), "level score", PERMS_PRIVATE);
+}
 
-  if (program.global_scores)
-    createDirectory(getCommonDataDir(), "common data", permissions);
-  else
-    createDirectory(getMainUserGameDataDir(), "main user data", permissions);
+void InitScoreCacheDirectory(char *level_subdir)
+{
+  createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
+  createDirectory(getCacheDir(), "cache data", PERMS_PRIVATE);
+  createDirectory(getScoreCacheDir(NULL), "main score", PERMS_PRIVATE);
+  createDirectory(getScoreCacheDir(level_subdir), "level score", PERMS_PRIVATE);
+}
+
+void InitScoreTapeDirectory(char *level_subdir, int nr)
+{
+  InitScoreDirectory(level_subdir);
 
-  createDirectory(getScoreDir(NULL), "main score", permissions);
-  createDirectory(getScoreDir(level_subdir), "level score", permissions);
+  createDirectory(getScoreTapeDir(level_subdir, nr), "score tape", PERMS_PRIVATE);
 }
 
 static void SaveUserLevelInfo(void);
@@ -1181,22 +1323,35 @@ TreeInfo *getValidLevelSeries(TreeInfo *node, TreeInfo *default_node)
     return getFirstValidTreeInfoEntry(default_node);
 }
 
-TreeInfo *getFirstValidTreeInfoEntry(TreeInfo *node)
+static TreeInfo *getValidTreeInfoEntryExt(TreeInfo *node, boolean get_next_node)
 {
   if (node == NULL)
     return NULL;
 
-  if (node->node_group)                // enter level group (step down into tree)
+  if (node->node_group)                // enter node group (step down into tree)
     return getFirstValidTreeInfoEntry(node->node_group);
-  else if (node->parent_link)  // skip start entry of level group
-  {
-    if (node->next)            // get first real level series entry
-      return getFirstValidTreeInfoEntry(node->next);
-    else                       // leave empty level group and go on
-      return getFirstValidTreeInfoEntry(node->node_parent->next);
-  }
-  else                         // this seems to be a regular level series
+
+  if (node->parent_link)       // skip first node (back link) of node group
+    get_next_node = TRUE;
+
+  if (!get_next_node)          // get current regular tree node
     return node;
+
+  // get next regular tree node, or step up until one is found
+  while (node->next == NULL && node->node_parent != NULL)
+    node = node->node_parent;
+
+  return getFirstValidTreeInfoEntry(node->next);
+}
+
+TreeInfo *getFirstValidTreeInfoEntry(TreeInfo *node)
+{
+  return getValidTreeInfoEntryExt(node, FALSE);
+}
+
+TreeInfo *getNextValidTreeInfoEntry(TreeInfo *node)
+{
+  return getValidTreeInfoEntryExt(node, TRUE);
 }
 
 TreeInfo *getTreeInfoFirstGroupEntry(TreeInfo *node)
@@ -1382,9 +1537,10 @@ static boolean adjustTreeSoundsForEMC(TreeInfo *node)
   return settings_changed;
 }
 
-void dumpTreeInfo(TreeInfo *node, int depth)
+int dumpTreeInfo(TreeInfo *node, int depth)
 {
   char bullet_list[] = { '-', '*', 'o' };
+  int num_leaf_nodes = 0;
   int i;
 
   if (depth == 0)
@@ -1400,7 +1556,11 @@ void dumpTreeInfo(TreeInfo *node, int depth)
     DebugContinued("tree", "%c '%s' ['%s] [PARENT: '%s'] %s\n",
                   bullet, node->name, node->identifier,
                   (node->node_parent ? node->node_parent->identifier : "-"),
-                  (node->node_group ? "[GROUP]" : ""));
+                  (node->node_group ? "[GROUP]" :
+                   node->is_copy ? "[COPY]" : ""));
+
+    if (!node->node_group && !node->parent_link)
+      num_leaf_nodes++;
 
     /*
     // use for dumping artwork info tree
@@ -1409,10 +1569,15 @@ void dumpTreeInfo(TreeInfo *node, int depth)
     */
 
     if (node->node_group != NULL)
-      dumpTreeInfo(node->node_group, depth + 1);
+      num_leaf_nodes += dumpTreeInfo(node->node_group, depth + 1);
 
     node = node->next;
   }
+
+  if (depth == 0)
+    Debug("tree", "Summary: %d leaf nodes found", num_leaf_nodes);
+
+  return num_leaf_nodes;
 }
 
 void sortTreeInfoBySortFunction(TreeInfo **node_first,
@@ -1553,29 +1718,6 @@ char *getHomeDir(void)
   return dir;
 }
 
-char *getCommonDataDir(void)
-{
-  static char *common_data_dir = NULL;
-
-#if defined(PLATFORM_WIN32)
-  if (common_data_dir == NULL)
-  {
-    char *dir = checked_malloc(MAX_PATH + 1);
-
-    if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_COMMON_DOCUMENTS, NULL, 0, dir))
-       && !strEqual(dir, ""))          // empty for Windows 95/98
-      common_data_dir = getPath2(dir, program.userdata_subdir);
-    else
-      common_data_dir = options.rw_base_directory;
-  }
-#else
-  if (common_data_dir == NULL)
-    common_data_dir = options.rw_base_directory;
-#endif
-
-  return common_data_dir;
-}
-
 char *getPersonalDataDir(void)
 {
   static char *personal_data_dir = NULL;
@@ -3490,7 +3632,7 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
     (leveldir_new->user_defined || !leveldir_new->handicap ?
      leveldir_new->last_level : leveldir_new->first_level);
 
-  DrawInitText(leveldir_new->name, 150, FC_YELLOW);
+  DrawInitTextItem(leveldir_new->name);
 
   pushTreeInfo(node_first, leveldir_new);
 
@@ -3606,7 +3748,7 @@ void LoadLevelInfo(void)
 {
   InitUserLevelDirectory(getLoginName());
 
-  DrawInitText("Loading level series", 120, FC_GREEN);
+  DrawInitTextHead("Loading level series");
 
   LoadLevelInfoFromLevelDir(&leveldir_first, NULL, options.level_directory);
   LoadLevelInfoFromLevelDir(&leveldir_first, NULL, getUserLevelDir(NULL));
@@ -3879,7 +4021,7 @@ void LoadArtworkInfo(void)
 {
   LoadArtworkInfoCache();
 
-  DrawInitText("Looking for custom artwork", 120, FC_GREEN);
+  DrawInitTextHead("Looking for custom artwork");
 
   LoadArtworkInfoFromArtworkDir(&artwork.gfx_first, NULL,
                                options.graphics_directory,
@@ -4014,7 +4156,7 @@ static void LoadArtworkInfoFromLevelInfoExt(ArtworkDirTree **artwork_node,
        setArtworkInfoCacheEntry(artwork_new, level_node, type);
     }
 
-    DrawInitText(level_node->name, 150, FC_YELLOW);
+    DrawInitTextItem(level_node->name);
 
     if (level_node->node_group != NULL)
     {
@@ -4083,7 +4225,7 @@ void LoadLevelArtworkInfo(void)
 {
   print_timestamp_init("LoadLevelArtworkInfo");
 
-  DrawInitText("Looking for custom level artwork", 120, FC_GREEN);
+  DrawInitTextHead("Looking for custom level artwork");
 
   print_timestamp_time("DrawTimeText");
 
@@ -4739,7 +4881,7 @@ static void SaveLevelSetup_LastSeries_Ext(boolean deactivate_last_level_series)
 
   for (i = 0; last_level_series[i] != NULL; i++)
   {
-    char token[strlen(TOKEN_STR_LAST_LEVEL_SERIES) + 10];
+    char token[strlen(TOKEN_STR_LAST_LEVEL_SERIES) + 1 + 10 + 1];
 
     sprintf(token, "%s.%03d", TOKEN_STR_LAST_LEVEL_SERIES, i);
 
index c8c5fec..ab52a7a 100644 (file)
@@ -264,8 +264,14 @@ char *setLevelArtworkDir(TreeInfo *);
 char *getProgramMainDataPath(char *, char *);
 char *getProgramConfigFilename(char *);
 char *getTapeFilename(int);
+char *getTemporaryTapeFilename(void);
+char *getDefaultSolutionTapeFilename(int);
+char *getSokobanSolutionTapeFilename(int);
 char *getSolutionTapeFilename(int);
 char *getScoreFilename(int);
+char *getScoreCacheFilename(int);
+char *getScoreTapeBasename(char *);
+char *getScoreTapeFilename(char *, int);
 char *getSetupFilename(void);
 char *getDefaultSetupFilename(void);
 char *getEditorSetupFilename(void);
@@ -282,8 +288,14 @@ char *getCustomArtworkConfigFilename(int);
 char *getCustomArtworkLevelConfigFilename(int);
 char *getCustomMusicDirectory(void);
 
+void MarkTapeDirectoryUploadsAsComplete(char *);
+void MarkTapeDirectoryUploadsAsIncomplete(char *);
+boolean CheckTapeDirectoryUploadsComplete(char *);
+
 void InitTapeDirectory(char *);
 void InitScoreDirectory(char *);
+void InitScoreCacheDirectory(char *);
+void InitScoreTapeDirectory(char *, int);
 void InitUserLevelDirectory(char *);
 void InitNetworkLevelDirectory(char *);
 void InitLevelSetupDirectory(char *);
@@ -296,12 +308,13 @@ int numTreeInfo(TreeInfo *);
 boolean validLevelSeries(TreeInfo *);
 TreeInfo *getValidLevelSeries(TreeInfo *, TreeInfo *);
 TreeInfo *getFirstValidTreeInfoEntry(TreeInfo *);
+TreeInfo *getNextValidTreeInfoEntry(TreeInfo *);
 TreeInfo *getTreeInfoFirstGroupEntry(TreeInfo *);
 int numTreeInfoInGroup(TreeInfo *);
 int getPosFromTreeInfo(TreeInfo *);
 TreeInfo *getTreeInfoFromPos(TreeInfo *, int);
 TreeInfo *getTreeInfoFromIdentifier(TreeInfo *, char *);
-void dumpTreeInfo(TreeInfo *, int);
+int dumpTreeInfo(TreeInfo *, int);
 void sortTreeInfoBySortFunction(TreeInfo **,
                                int (*compare_function)(const void *,
                                                        const void *));
@@ -309,7 +322,6 @@ void sortTreeInfo(TreeInfo **);
 void freeTreeInfo(TreeInfo *);
 
 char *getHomeDir(void);
-char *getCommonDataDir(void);
 char *getPersonalDataDir(void);
 char *getMainUserGameDataDir(void);
 char *getUserGameDataDir(void);
@@ -319,6 +331,7 @@ char *getUserLevelDir(char *);
 char *getNetworkLevelDir(char *);
 char *getCurrentLevelDir(void);
 char *getNewUserLevelSubdir(void);
+char *getTapeDir(char *);
 
 void createDirectory(char *, char *, int);
 void InitMainUserDataDirectory(void);
index 2375c06..d7ec950 100644 (file)
@@ -619,7 +619,7 @@ static void LoadCustomMusic_NoConf(void)
   }
 
   if (draw_init_text)
-    DrawInitText("Loading music", 120, FC_GREEN);
+    DrawInitTextHead("Loading music");
 
   while ((dir_entry = readDirectory(dir)) != NULL)     // loop all entries
   {
@@ -644,7 +644,7 @@ static void LoadCustomMusic_NoConf(void)
       continue;
 
     if (draw_init_text)
-      DrawInitText(basename, 150, FC_YELLOW);
+      DrawInitTextItem(basename);
 
     if (FileIsMusic(dir_entry->filename))
       mus_info = Load_WAV_or_MOD(dir_entry->filename);
index a2f7ebe..3eb9c62 100644 (file)
@@ -103,26 +103,9 @@ void InitProgramInfo(char *argv0, char *config_filename, char *userdata_subdir,
   program.log_file[LOG_OUT_ID] = program.log_file_default[LOG_OUT_ID] = stdout;
   program.log_file[LOG_ERR_ID] = program.log_file_default[LOG_ERR_ID] = stderr;
 
-  program.headless = FALSE;
-
-#if defined(PLATFORM_EMSCRIPTEN)
-  EM_ASM
-  (
-    Module.sync_done = 0;
-
-    FS.mkdir('/persistent');           // create persistent data directory
-    FS.mount(IDBFS, {}, '/persistent');        // mount with IDBFS filesystem type
-    FS.syncfs(true, function(err)      // sync persistent data into memory
-    {
-      assert(!err);
-      Module.sync_done = 1;
-    });
-  );
+  program.api_thread_count = 0;
 
-  // wait for persistent data to be synchronized to memory
-  while (emscripten_run_script_int("Module.sync_done") == 0)
-    Delay(20);
-#endif
+  program.headless = FALSE;
 }
 
 void InitNetworkInfo(boolean enabled, boolean connected, boolean serveronly,
@@ -146,33 +129,8 @@ void InitRuntimeInfo()
 #else
   runtime.uses_touch_device = FALSE;
 #endif
-}
-
-void InitScoresInfo(void)
-{
-  char *global_scores_dir = getPath2(getCommonDataDir(), SCORES_DIRECTORY);
-
-  program.global_scores = directoryExists(global_scores_dir);
-  program.many_scores_per_name = !program.global_scores;
-
-#if 0
-  if (options.debug)
-  {
-    if (program.global_scores)
-    {
-      Debug("internal:path", "Using global, multi-user scores directory '%s'.",
-           global_scores_dir);
-      Debug("internal:path", "Remove to enable single-user scores directory.");
-      Debug("internal:path", "(This enables multipe score entries per user.)");
-    }
-    else
-    {
-      Debug("internal:path", "Using private, single-user scores directory.");
-    }
-  }
-#endif
 
-  free(global_scores_dir);
+  runtime.use_api_server = setup.use_api_server;
 }
 
 void SetWindowTitle(void)
@@ -206,6 +164,8 @@ void InitExitFunction(void (*exit_function)(int))
 
 void InitPlatformDependentStuff(void)
 {
+  InitEmscriptenFilesystem();
+
   // this is initialized in GetOptions(), but may already be used before
   options.verbose = TRUE;
 
@@ -1235,7 +1195,7 @@ void ReloadCustomImage(Bitmap *bitmap, char *basename)
   free(new_bitmap);
 }
 
-static Bitmap *ZoomBitmap(Bitmap *src_bitmap, int zoom_width, int zoom_height)
+Bitmap *ZoomBitmap(Bitmap *src_bitmap, int zoom_width, int zoom_height)
 {
   return SDLZoomBitmap(src_bitmap, zoom_width, zoom_height);
 }
@@ -1244,14 +1204,31 @@ void ReCreateGameTileSizeBitmap(Bitmap **bitmaps)
 {
   if (bitmaps[IMG_BITMAP_CUSTOM])
   {
-    FreeBitmap(bitmaps[IMG_BITMAP_CUSTOM]);
+    // check if original sized bitmap points to custom sized bitmap
+    if (bitmaps[IMG_BITMAP_PTR_ORIGINAL] == bitmaps[IMG_BITMAP_CUSTOM])
+    {
+      SDLFreeBitmapTextures(bitmaps[IMG_BITMAP_PTR_ORIGINAL]);
+
+      // keep pointer of previous custom size bitmap
+      bitmaps[IMG_BITMAP_OTHER] = bitmaps[IMG_BITMAP_CUSTOM];
+
+      // set original bitmap pointer to scaled original bitmap of other size
+      bitmaps[IMG_BITMAP_PTR_ORIGINAL] = bitmaps[IMG_BITMAP_OTHER];
+
+      SDLCreateBitmapTextures(bitmaps[IMG_BITMAP_PTR_ORIGINAL]);
+    }
+    else
+    {
+      FreeBitmap(bitmaps[IMG_BITMAP_CUSTOM]);
+    }
 
     bitmaps[IMG_BITMAP_CUSTOM] = NULL;
   }
 
   if (gfx.game_tile_size == gfx.standard_tile_size)
   {
-    bitmaps[IMG_BITMAP_GAME] = bitmaps[IMG_BITMAP_STANDARD];
+    // set game bitmap pointer to standard sized bitmap (already existing)
+    bitmaps[IMG_BITMAP_PTR_GAME] = bitmaps[IMG_BITMAP_STANDARD];
 
     return;
   }
@@ -1260,10 +1237,10 @@ void ReCreateGameTileSizeBitmap(Bitmap **bitmaps)
   int width  = bitmap->width  * gfx.game_tile_size / gfx.standard_tile_size;;
   int height = bitmap->height * gfx.game_tile_size / gfx.standard_tile_size;;
 
-  Bitmap *bitmap_new = ZoomBitmap(bitmap, width, height);
+  bitmaps[IMG_BITMAP_CUSTOM] = ZoomBitmap(bitmap, width, height);
 
-  bitmaps[IMG_BITMAP_CUSTOM] = bitmap_new;
-  bitmaps[IMG_BITMAP_GAME]   = bitmap_new;
+  // set game bitmap pointer to custom sized bitmap (newly created)
+  bitmaps[IMG_BITMAP_PTR_GAME] = bitmaps[IMG_BITMAP_CUSTOM];
 }
 
 static void CreateScaledBitmaps(Bitmap **bitmaps, int zoom_factor,
@@ -1413,9 +1390,33 @@ static void CreateScaledBitmaps(Bitmap **bitmaps, int zoom_factor,
       bitmaps[IMG_BITMAP_CUSTOM] = tmp_bitmap_0;
 
     if (bitmaps[IMG_BITMAP_CUSTOM])
-      bitmaps[IMG_BITMAP_GAME] = bitmaps[IMG_BITMAP_CUSTOM];
+      bitmaps[IMG_BITMAP_PTR_GAME] = bitmaps[IMG_BITMAP_CUSTOM];
+    else
+      bitmaps[IMG_BITMAP_PTR_GAME] = bitmaps[IMG_BITMAP_STANDARD];
+
+    // store the "final" (up-scaled) original bitmap, if not already stored
+
+    int tmp_bitmap_final_nr = -1;
+
+    for (i = 0; i < NUM_IMG_BITMAPS; i++)
+      if (bitmaps[i] == tmp_bitmap_final)
+       tmp_bitmap_final_nr = i;
+
+    if (tmp_bitmap_final_nr == -1)     // scaled original bitmap not stored
+    {
+      // store pointer of scaled original bitmap (not used for any other size)
+      bitmaps[IMG_BITMAP_OTHER] = tmp_bitmap_final;
+
+      // set original bitmap pointer to scaled original bitmap of other size
+      bitmaps[IMG_BITMAP_PTR_ORIGINAL] = bitmaps[IMG_BITMAP_OTHER];
+    }
     else
-      bitmaps[IMG_BITMAP_GAME] = bitmaps[IMG_BITMAP_STANDARD];
+    {
+      // set original bitmap pointer to corresponding sized bitmap
+      bitmaps[IMG_BITMAP_PTR_ORIGINAL] = bitmaps[tmp_bitmap_final_nr];
+    }
+
+    // free the "old" (unscaled) original bitmap, if not already stored
 
     boolean free_old_bitmap = TRUE;
 
@@ -1435,6 +1436,12 @@ static void CreateScaledBitmaps(Bitmap **bitmaps, int zoom_factor,
   else
   {
     bitmaps[IMG_BITMAP_32x32] = tmp_bitmap_1;
+
+    // set original bitmap pointer to corresponding sized bitmap
+    bitmaps[IMG_BITMAP_PTR_ORIGINAL] = bitmaps[IMG_BITMAP_32x32];
+
+    if (old_bitmap != tmp_bitmap_1)
+      FreeBitmap(old_bitmap);
   }
 
   UPDATE_BUSY_STATE();
@@ -1450,12 +1457,18 @@ void CreateBitmapWithSmallBitmaps(Bitmap **bitmaps, int zoom_factor,
 
 void CreateBitmapTextures(Bitmap **bitmaps)
 {
-  SDLCreateBitmapTextures(bitmaps[IMG_BITMAP_STANDARD]);
+  if (bitmaps[IMG_BITMAP_PTR_ORIGINAL] != NULL)
+    SDLCreateBitmapTextures(bitmaps[IMG_BITMAP_PTR_ORIGINAL]);
+  else
+    SDLCreateBitmapTextures(bitmaps[IMG_BITMAP_STANDARD]);
 }
 
 void FreeBitmapTextures(Bitmap **bitmaps)
 {
-  SDLFreeBitmapTextures(bitmaps[IMG_BITMAP_STANDARD]);
+  if (bitmaps[IMG_BITMAP_PTR_ORIGINAL] != NULL)
+    SDLFreeBitmapTextures(bitmaps[IMG_BITMAP_PTR_ORIGINAL]);
+  else
+    SDLFreeBitmapTextures(bitmaps[IMG_BITMAP_STANDARD]);
 }
 
 void ScaleBitmap(Bitmap **bitmaps, int zoom_factor)
@@ -1867,3 +1880,43 @@ void ClearJoystickState(void)
 {
   SDLClearJoystickState();
 }
+
+
+// ============================================================================
+// Emscripten functions
+// ============================================================================
+
+void InitEmscriptenFilesystem(void)
+{
+#if defined(PLATFORM_EMSCRIPTEN)
+  EM_ASM
+  (
+    Module.sync_done = 0;
+
+    FS.mkdir('/persistent');           // create persistent data directory
+    FS.mount(IDBFS, {}, '/persistent');        // mount with IDBFS filesystem type
+    FS.syncfs(true, function(err)      // sync persistent data into memory
+    {
+      assert(!err);
+      Module.sync_done = 1;
+    });
+  );
+
+  // wait for persistent data to be synchronized to memory
+  while (emscripten_run_script_int("Module.sync_done") == 0)
+    Delay(20);
+#endif
+}
+
+void SyncEmscriptenFilesystem(void)
+{
+#if defined(PLATFORM_EMSCRIPTEN)
+  EM_ASM
+  (
+    FS.syncfs(function(err)
+    {
+      assert(!err);
+    });
+  );
+#endif
+}
index 2db4961..8ad5c08 100644 (file)
 #define STR_NETWORK_AUTO_DETECT                "auto_detect_network_server"
 #define STR_NETWORK_AUTO_DETECT_SETUP  "(auto detect network server)"
 
+// values for API server settings
+#define API_SERVER_HOSTNAME            "api.artsoft.org"
+#define API_SERVER_PORT                        80
+#define API_SERVER_METHOD              "POST"
+#define API_SERVER_URI_ADD             "/api/scores/add"
+#define API_SERVER_URI_GET             "/api/scores/get"
+#define API_SERVER_URI_RENAME          "/api/players/rename"
+
+#if defined(TESTING)
+#undef API_SERVER_HOSTNAME
+#define API_SERVER_HOSTNAME            "api-test.artsoft.org"
+#define TEST_PREFIX                    "test."
+#else
+#define TEST_PREFIX                    ""
+#endif
+
 // values for touch control
 #define TOUCH_CONTROL_OFF              "off"
 #define TOUCH_CONTROL_VIRTUAL_BUTTONS  "virtual_buttons"
 // default value for undefined levelset
 #define UNDEFINED_LEVELSET     "[NONE]"
 
+// default value for undefined password
+#define UNDEFINED_PASSWORD     "[undefined]"
+
 // default value for undefined parameter
 #define ARG_DEFAULT            "[DEFAULT]"
 
 // default value for off-screen positions
 #define POS_OFFSCREEN          (-1000000)
 
-// definitions for game sub-directories
-#ifndef RO_GAME_DIR
-#define RO_GAME_DIR            "."
+// definitions for game base path and sub-directories
+#ifndef BASE_PATH
+#define BASE_PATH              "."
 #endif
 
-#ifndef RW_GAME_DIR
-#define RW_GAME_DIR            "."
-#endif
-
-#define RO_BASE_PATH           RO_GAME_DIR
-#define RW_BASE_PATH           RW_GAME_DIR
-
 // directory names
 #define GRAPHICS_DIRECTORY     "graphics"
 #define SOUNDS_DIRECTORY       "sounds"
 #define USERSETUP_FILENAME     "usersetup.conf"
 #define AUTOSETUP_FILENAME     "autosetup.conf"
 #define LEVELSETUP_FILENAME    "levelsetup.conf"
+#define SERVERSETUP_FILENAME   "serversetup.conf"
 #define EDITORSETUP_FILENAME   "editorsetup.conf"
 #define EDITORCASCADE_FILENAME "editorcascade.conf"
 #define HELPANIM_FILENAME      "helpanim.conf"
 #define MUSICINFO_FILENAME     "musicinfo.conf"
 #define ARTWORKINFO_CACHE_FILE "artworkinfo.cache"
 #define LEVELTEMPLATE_FILENAME "template.level"
+#define UPLOADED_FILENAME      ".uploaded"
 #define LEVELFILE_EXTENSION    "level"
 #define TAPEFILE_EXTENSION     "tape"
 #define SCOREFILE_EXTENSION    "score"
@@ -1010,8 +1024,7 @@ struct ProgramInfo
   void (*exit_message_function)(char *, va_list);
   void (*exit_function)(int);
 
-  boolean global_scores;
-  boolean many_scores_per_name;
+  int api_thread_count;
 
   boolean headless;
 };
@@ -1032,6 +1045,8 @@ struct NetworkInfo
 struct RuntimeInfo
 {
   boolean uses_touch_device;
+
+  boolean use_api_server;
 };
 
 struct OptionInfo
@@ -1039,8 +1054,7 @@ struct OptionInfo
   char *server_host;
   int server_port;
 
-  char *ro_base_directory;
-  char *rw_base_directory;
+  char *base_directory;
   char *level_directory;
   char *graphics_directory;
   char *sounds_directory;
@@ -1049,10 +1063,15 @@ struct OptionInfo
   char *conf_directory;
 
   char *execute_command;
+  char *tape_log_filename;
 
   char *special_flags;
   char *debug_mode;
 
+  char *player_name;
+  char *identifier;
+  char *level_nr;
+
   boolean mytapes;
   boolean serveronly;
   boolean network;
@@ -1428,6 +1447,7 @@ struct SetupDebugInfo
 struct SetupInfo
 {
   char *player_name;
+  char *player_uuid;
 
   boolean multiple_users;
 
@@ -1472,7 +1492,9 @@ struct SetupInfo
   int game_frame_delay;
   boolean sp_show_border_elements;
   boolean small_game_graphics;
-  boolean show_snapshot_buttons;
+  boolean show_load_save_buttons;
+  boolean show_undo_redo_buttons;
+  char *scores_in_highscore_list;
 
   char *graphics_set;
   char *sounds_set;
@@ -1489,6 +1511,15 @@ struct SetupInfo
   int network_player_nr;
   char *network_server_hostname;
 
+  boolean use_api_server;
+  char *api_server_hostname;
+  char *api_server_password;
+  boolean ask_for_uploading_tapes;
+  boolean ask_for_remaining_tapes;
+  boolean provide_uploading_tapes;
+  boolean ask_for_using_api_server;
+  boolean has_remaining_tapes;
+
   struct SetupAutoSetupInfo auto_setup;
   struct SetupLevelSetupInfo level_setup;
 
@@ -1862,7 +1893,6 @@ void InitProgramInfo(char *, char *, char *, char *, char *, char *, char *,
 void InitNetworkInfo(boolean, boolean, boolean, char *, int);
 void InitRuntimeInfo(void);
 
-void InitScoresInfo(void);
 void SetWindowTitle(void);
 
 void InitWindowTitleFunction(char *(*window_title_function)(void));
@@ -1951,6 +1981,7 @@ Bitmap *LoadImage(char *);
 Bitmap *LoadCustomImage(char *);
 void ReloadCustomImage(Bitmap *, char *);
 
+Bitmap *ZoomBitmap(Bitmap *, int, int);
 void ReCreateGameTileSizeBitmap(Bitmap **);
 void CreateBitmapWithSmallBitmaps(Bitmap **, int, int);
 void CreateBitmapTextures(Bitmap **);
@@ -1984,4 +2015,7 @@ boolean ReadJoystick(int, int *, int *, boolean *, boolean *);
 boolean CheckJoystickOpened(int);
 void ClearJoystickState(void);
 
+void InitEmscriptenFilesystem(void);
+void SyncEmscriptenFilesystem(void);
+
 #endif // SYSTEM_H
index c10583d..17475e8 100644 (file)
@@ -136,7 +136,7 @@ int maxWordLengthInRequestString(char *text)
 // simple text drawing functions
 // ============================================================================
 
-void DrawInitText(char *text, int ypos, int font_nr)
+static void DrawInitTextExt(char *text, int ypos, int font_nr, boolean update)
 {
   LimitScreenUpdates(TRUE);
 
@@ -155,10 +155,26 @@ void DrawInitText(char *text, int ypos, int font_nr)
     ClearRectangle(drawto, 0, y, width, height);
     DrawTextExt(drawto, x, y, text, font_nr, BLIT_OPAQUE);
 
-    BlitBitmap(drawto, window, 0, 0, video.width, video.height, 0, 0);
+    if (update)
+      BlitBitmap(drawto, window, 0, 0, video.width, video.height, 0, 0);
   }
 }
 
+void DrawInitText(char *text, int ypos, int font_nr)
+{
+  DrawInitTextExt(text, ypos, font_nr, FALSE);
+}
+
+void DrawInitTextHead(char *text)
+{
+  DrawInitTextExt(text, 120, FC_GREEN, FALSE);
+}
+
+void DrawInitTextItem(char *text)
+{
+  DrawInitTextExt(text, 150, FC_YELLOW, TRUE);
+}
+
 void DrawTextF(int x, int y, int font_nr, char *format, ...)
 {
   char buffer[MAX_OUTPUT_LINESIZE + 1];
index 26df781..407cb31 100644 (file)
@@ -92,6 +92,8 @@ void getFontCharSource(int, char, Bitmap **, int *, int *);
 int maxWordLengthInRequestString(char *);
 
 void DrawInitText(char *, int, int);
+void DrawInitTextHead(char *);
+void DrawInitTextItem(char *);
 
 void DrawTextF(int, int, int, char *, ...);
 void DrawTextFCentered(int, int, char *, ...);
index 13f1ed8..8dd3712 100644 (file)
@@ -128,7 +128,7 @@ boolean                     network_player_action_received = FALSE;
 
 struct LevelInfo       level, level_template;
 struct PlayerInfo      stored_player[MAX_PLAYERS], *local_player = NULL;
-struct HiScore         highscore[MAX_SCORE_ENTRIES];
+struct ScoreInfo       scores, server_scores;
 struct TapeInfo                tape;
 struct GameInfo                game;
 struct GlobalInfo      global;
@@ -7641,7 +7641,8 @@ static void print_usage(void)
        "  \"autofix LEVELDIR [NR ...]\"      test and fix tapes for LEVELDIR\n"
        "  \"patch tapes MODE LEVELDIR [NR]\" patch level tapes for LEVELDIR\n"
        "  \"convert LEVELDIR [NR]\"          convert levels in LEVELDIR\n"
-       "  \"create images DIRECTORY\"        write BMP images to DIRECTORY\n"
+       "  \"create sketch images DIRECTORY\" write BMP images to DIRECTORY\n"
+       "  \"create collect image DIRECTORY\" write BMP image to DIRECTORY\n"
        "  \"create CE image DIRECTORY\"      write BMP image to DIRECTORY\n"
        "\n",
        program.command_basename);
index c67fbeb..bf92901 100644 (file)
 #define GFX_CRUMBLED(e)                HAS_PROPERTY(GFX_ELEMENT(e), EP_GFX_CRUMBLED)
 
 // macros for pre-defined properties
-#define ELEM_IS_PLAYER(e)      HAS_PROPERTY(e, EP_PLAYER)
+#define IS_PLAYER_ELEMENT(e)   HAS_PROPERTY(e, EP_PLAYER)
 #define CAN_PASS_MAGIC_WALL(e) HAS_PROPERTY(e, EP_CAN_PASS_MAGIC_WALL)
 #define CAN_PASS_DC_MAGIC_WALL(e) HAS_PROPERTY(e, EP_CAN_PASS_DC_MAGIC_WALL)
 #define IS_SWITCHABLE(e)       HAS_PROPERTY(e, EP_SWITCHABLE)
        (ge == EL_ANY_ELEMENT ? TRUE :                                  \
         IS_GROUP_ELEMENT(ge) ? IS_IN_GROUP(e, GROUP_NR(ge)) : (e) == (ge))
 
-#define IS_PLAYER(x, y)                (ELEM_IS_PLAYER(StorePlayer[x][y]))
+#define IS_PLAYER(x, y)                (IS_PLAYER_ELEMENT(StorePlayer[x][y]))
 
 #define IS_FREE(x, y)          (Tile[x][y] == EL_EMPTY && !IS_PLAYER(x, y))
 #define IS_FREE_OR_PLAYER(x, y)        (Tile[x][y] == EL_EMPTY)
@@ -2574,9 +2574,9 @@ enum
 
 // program information and versioning definitions
 #define PROGRAM_VERSION_SUPER          4
-#define PROGRAM_VERSION_MAJOR          2
-#define PROGRAM_VERSION_MINOR          3
-#define PROGRAM_VERSION_PATCH          1
+#define PROGRAM_VERSION_MAJOR          3
+#define PROGRAM_VERSION_MINOR          0
+#define PROGRAM_VERSION_PATCH          2
 #define PROGRAM_VERSION_EXTRA          ""
 
 #define PROGRAM_TITLE_STRING           "Rocks'n'Diamonds"
@@ -2624,7 +2624,7 @@ enum
 // values for game_emulation
 #define EMU_NONE                       0
 #define EMU_BOULDERDASH                        1
-#define EMU_SOKOBAN                    2
+#define EMU_UNUSED_2                   2
 #define EMU_SUPAPLEX                   3
 
 // values for level file type identifier
@@ -2655,7 +2655,9 @@ enum
 #define AUTOPLAY_FFWD                  (1 << 1)
 #define AUTOPLAY_WARP                  (1 << 2)
 #define AUTOPLAY_TEST                  (1 << 3)
-#define AUTOPLAY_FIX                   (1 << 4)
+#define AUTOPLAY_SAVE                  (1 << 4)
+#define AUTOPLAY_UPLOAD                        (1 << 5)
+#define AUTOPLAY_FIX                   (1 << 6)
 #define AUTOPLAY_WARP_NO_DISPLAY       AUTOPLAY_TEST
 
 #define AUTOPLAY_MODE_NONE             0
@@ -2663,6 +2665,8 @@ enum
 #define AUTOPLAY_MODE_FFWD             (AUTOPLAY_MODE_PLAY | AUTOPLAY_FFWD)
 #define AUTOPLAY_MODE_WARP             (AUTOPLAY_MODE_FFWD | AUTOPLAY_WARP)
 #define AUTOPLAY_MODE_TEST             (AUTOPLAY_MODE_WARP | AUTOPLAY_TEST)
+#define AUTOPLAY_MODE_SAVE             (AUTOPLAY_MODE_TEST | AUTOPLAY_SAVE)
+#define AUTOPLAY_MODE_UPLOAD           (AUTOPLAY_MODE_TEST | AUTOPLAY_UPLOAD)
 #define AUTOPLAY_MODE_FIX              (AUTOPLAY_MODE_TEST | AUTOPLAY_FIX)
 #define AUTOPLAY_MODE_WARP_NO_DISPLAY  AUTOPLAY_MODE_TEST
 
@@ -3038,10 +3042,31 @@ struct ViewportInfo
   struct RectWithBorder door_2[NUM_SPECIAL_GFX_ARGS];
 };
 
-struct HiScore
+struct ScoreEntry
 {
-  char Name[MAX_PLAYER_NAME_LEN + 1];
-  int Score;
+  char tape_basename[MAX_FILENAME_LEN + 1];
+  char name[MAX_PLAYER_NAME_LEN + 1];
+  int score;
+  int time;            // time (in frames) or steps played
+};
+
+struct ScoreInfo
+{
+  int file_version;    // file format version the score is stored with
+  int game_version;    // game release version the score was created with
+
+  char level_identifier[MAX_FILENAME_LEN + 1];
+  int level_nr;
+
+  int num_entries;
+  int last_added;
+  int last_added_local;
+
+  boolean updated;
+  boolean uploaded;
+  boolean force_last_added;
+
+  struct ScoreEntry entry[MAX_SCORE_ENTRIES];
 };
 
 struct Content
@@ -3107,6 +3132,7 @@ struct LevelInfo
   int time;                            // available time (seconds)
   int gems_needed;
   boolean auto_count_gems;
+  boolean rate_time_over_score;
 
   char name[MAX_LEVEL_NAME_LEN + 1];
   char author[MAX_LEVEL_AUTHOR_LEN + 1];
@@ -3192,6 +3218,7 @@ struct LevelInfo
   boolean auto_exit_sokoban;   // automatically finish solved Sokoban levels
   boolean solved_by_one_player;        // level is solved if one player enters exit
   boolean finish_dig_collect;  // only finished dig/collect triggers ce action
+  boolean keep_walkable_ce;    // keep walkable CE if it changes to the player
 
   boolean continuous_snapping; // repeated snapping without releasing key
   boolean block_snap_field;    // snapping blocks field to show animation
@@ -3241,8 +3268,9 @@ struct GlobalInfo
 {
   char *autoplay_leveldir;
   int autoplay_level[MAX_TAPES_PER_SET];
+  int autoplay_mode;
   boolean autoplay_all;
-  boolean autoplay_mode;
+  time_t autoplay_time;
 
   char *patchtapes_mode;
   char *patchtapes_leveldir;
@@ -3252,7 +3280,14 @@ struct GlobalInfo
   char *convert_leveldir;
   int convert_level_nr;
 
-  char *create_images_dir;
+  char *dumplevel_leveldir;
+  int dumplevel_level_nr;
+
+  char *dumptape_leveldir;
+  int dumptape_level_nr;
+
+  char *create_sketch_images_dir;
+  char *create_collect_images_dir;
 
   int num_toons;
 
@@ -3749,7 +3784,7 @@ extern boolean                    network_player_action_received;
 extern int                     graphics_action_mapping[];
 
 extern struct LevelInfo                level, level_template;
-extern struct HiScore          highscore[];
+extern struct ScoreInfo                scores, server_scores;
 extern struct TapeInfo         tape;
 extern struct GlobalInfo       global;
 extern struct BorderInfo       border;
index 464840b..e34a9df 100644 (file)
 #define SETUP_MODE_CHOOSE_OTHER                16
 
 // sub-screens on the setup screen (specific)
-#define SETUP_MODE_CHOOSE_GAME_SPEED   17
-#define SETUP_MODE_CHOOSE_SCROLL_DELAY 18
-#define SETUP_MODE_CHOOSE_SNAPSHOT_MODE        19
-#define SETUP_MODE_CHOOSE_WINDOW_SIZE  20
-#define SETUP_MODE_CHOOSE_SCALING_TYPE 21
-#define SETUP_MODE_CHOOSE_RENDERING    22
-#define SETUP_MODE_CHOOSE_VSYNC                23
-#define SETUP_MODE_CHOOSE_GRAPHICS     24
-#define SETUP_MODE_CHOOSE_SOUNDS       25
-#define SETUP_MODE_CHOOSE_MUSIC                26
-#define SETUP_MODE_CHOOSE_VOLUME_SIMPLE        27
-#define SETUP_MODE_CHOOSE_VOLUME_LOOPS 28
-#define SETUP_MODE_CHOOSE_VOLUME_MUSIC 29
-#define SETUP_MODE_CHOOSE_TOUCH_CONTROL        30
-#define SETUP_MODE_CHOOSE_MOVE_DISTANCE        31
-#define SETUP_MODE_CHOOSE_DROP_DISTANCE        32
-#define SETUP_MODE_CHOOSE_TRANSPARENCY 33
-#define SETUP_MODE_CHOOSE_GRID_XSIZE_0 34
-#define SETUP_MODE_CHOOSE_GRID_YSIZE_0 35
-#define SETUP_MODE_CHOOSE_GRID_XSIZE_1 36
-#define SETUP_MODE_CHOOSE_GRID_YSIZE_1 37
-#define SETUP_MODE_CONFIG_VIRT_BUTTONS 38
-
-#define MAX_SETUP_MODES                        39
+#define SETUP_MODE_CHOOSE_SCORES_TYPE  17
+#define SETUP_MODE_CHOOSE_GAME_SPEED   18
+#define SETUP_MODE_CHOOSE_SCROLL_DELAY 19
+#define SETUP_MODE_CHOOSE_SNAPSHOT_MODE        20
+#define SETUP_MODE_CHOOSE_WINDOW_SIZE  21
+#define SETUP_MODE_CHOOSE_SCALING_TYPE 22
+#define SETUP_MODE_CHOOSE_RENDERING    23
+#define SETUP_MODE_CHOOSE_VSYNC                24
+#define SETUP_MODE_CHOOSE_GRAPHICS     25
+#define SETUP_MODE_CHOOSE_SOUNDS       26
+#define SETUP_MODE_CHOOSE_MUSIC                27
+#define SETUP_MODE_CHOOSE_VOLUME_SIMPLE        28
+#define SETUP_MODE_CHOOSE_VOLUME_LOOPS 29
+#define SETUP_MODE_CHOOSE_VOLUME_MUSIC 30
+#define SETUP_MODE_CHOOSE_TOUCH_CONTROL        31
+#define SETUP_MODE_CHOOSE_MOVE_DISTANCE        32
+#define SETUP_MODE_CHOOSE_DROP_DISTANCE        33
+#define SETUP_MODE_CHOOSE_TRANSPARENCY 34
+#define SETUP_MODE_CHOOSE_GRID_XSIZE_0 35
+#define SETUP_MODE_CHOOSE_GRID_YSIZE_0 36
+#define SETUP_MODE_CHOOSE_GRID_XSIZE_1 37
+#define SETUP_MODE_CHOOSE_GRID_YSIZE_1 38
+#define SETUP_MODE_CONFIG_VIRT_BUTTONS 39
+
+#define MAX_SETUP_MODES                        40
 
 #define MAX_MENU_MODES                 MAX(MAX_INFO_MODES, MAX_SETUP_MODES)
 
 #define STR_SETUP_EXIT                 "Exit"
 #define STR_SETUP_SAVE_AND_EXIT                "Save and Exit"
 
+#define STR_SETUP_CHOOSE_SCORES_TYPE   "Scores Type"
 #define STR_SETUP_CHOOSE_GAME_SPEED    "Game Speed"
 #define STR_SETUP_CHOOSE_SCROLL_DELAY  "Scroll Delay"
 #define STR_SETUP_CHOOSE_SNAPSHOT_MODE "Snapshot Mode"
@@ -282,6 +284,9 @@ static void MapScreenTreeGadgets(TreeInfo *);
 
 static void UpdateScreenMenuGadgets(int, boolean);
 
+static boolean OfferUploadTapes(void);
+static void execOfferUploadTapes(void);
+
 static struct GadgetInfo *screen_gadget[NUM_SCREEN_GADGETS];
 
 static int info_mode = INFO_MODE_MAIN;
@@ -305,6 +310,9 @@ static TreeInfo *scroll_delay_current = NULL;
 static TreeInfo *snapshot_modes = NULL;
 static TreeInfo *snapshot_mode_current = NULL;
 
+static TreeInfo *scores_types = NULL;
+static TreeInfo *scores_type_current = NULL;
+
 static TreeInfo *game_speeds_normal = NULL;
 static TreeInfo *game_speeds_extended = NULL;
 static TreeInfo *game_speeds = NULL;
@@ -389,6 +397,15 @@ static struct StringValueTextInfo vsync_modes_list[] =
   {    NULL,                            NULL           },
 };
 
+static struct StringValueTextInfo scores_types_list[] =
+{
+  {    STR_SCORES_TYPE_LOCAL_ONLY,         "Local scores only"         },
+  {    STR_SCORES_TYPE_SERVER_ONLY,        "Server scores only"        },
+  {    STR_SCORES_TYPE_LOCAL_AND_SERVER,   "Local and server scores"   },
+
+  {    NULL,                           NULL            },
+};
+
 static struct ValueTextInfo game_speeds_list_normal[] =
 {
   {    30,     "Very Slow"                     },
@@ -1739,15 +1756,10 @@ void DrawMainMenu(void)
 
   OpenDoor(DOOR_CLOSE_1 | DOOR_OPEN_2);
 
-#if defined(PLATFORM_EMSCRIPTEN)
-  EM_ASM
-  (
-    FS.syncfs(function(err)
-    {
-      assert(!err);
-    });
-  );
-#endif
+  SyncEmscriptenFilesystem();
+
+  // needed once to upload tapes (after program start or after user change)
+  CheckUploadTapes();
 }
 
 static void gotoTopLevelDir(void)
@@ -1965,6 +1977,16 @@ void HandleTitleScreen(int mx, int my, int dx, int dy, int button)
   }
 }
 
+static void HandleMainMenu_ToggleTeamMode(void)
+{
+  setup.team_mode = !setup.team_mode;
+
+  InitializeMainControls();
+  DrawCursorAndText_Main(MAIN_CONTROL_NAME, TRUE, FALSE);
+
+  DrawPreviewPlayers();
+}
+
 static void HandleMainMenu_SelectLevel(int step, int direction,
                                       int selected_level_nr)
 {
@@ -2114,9 +2136,16 @@ void HandleMainMenu(int mx, int my, int dx, int dy, int button)
       }
       else if (dx != 0)
       {
-       if (choice != MAIN_CONTROL_INFO &&
-           choice != MAIN_CONTROL_SETUP)
+       if (choice == MAIN_CONTROL_NAME)
+       {
+         // special case: cursor left or right pressed -- toggle team mode
+         HandleMainMenu_ToggleTeamMode();
+       }
+       else if (choice != MAIN_CONTROL_INFO &&
+                choice != MAIN_CONTROL_SETUP)
+       {
          HandleMainMenu_SelectLevel(1, dx, NO_DIRECT_LEVEL_SELECT);
+       }
       }
     }
     else
@@ -2129,12 +2158,7 @@ void HandleMainMenu(int mx, int my, int dx, int dy, int button)
            insideTextPosRect(main_controls[i].pos_text, mx - mSX, my - mSY))
        {
          // special case: menu text "name/team" clicked -- toggle team mode
-         setup.team_mode = !setup.team_mode;
-
-         InitializeMainControls();
-         DrawCursorAndText_Main(choice, TRUE, FALSE);
-
-         DrawPreviewPlayers();
+         HandleMainMenu_ToggleTeamMode();
        }
        else
        {
@@ -2180,7 +2204,7 @@ void HandleMainMenu(int mx, int my, int dx, int dy, int button)
 
        SetGameStatus(GAME_MODE_SCORES);
 
-       DrawHallOfFame(level_nr, -1);
+       DrawHallOfFame(level_nr);
       }
       else if (pos == MAIN_CONTROL_EDITOR)
       {
@@ -2225,9 +2249,13 @@ void HandleMainMenu(int mx, int my, int dx, int dy, int button)
        SaveLevelSetup_LastSeries();
        SaveLevelSetup_SeriesInfo();
 
+#if defined(PLATFORM_EMSCRIPTEN)
+       Request("Close the browser window to quit!", REQ_CONFIRM);
+#else
        if (!setup.ask_on_quit_program ||
            Request("Do you really want to quit?", REQ_ASK | REQ_STAY_CLOSED))
          SetGameStatus(GAME_MODE_QUIT);
+#endif
       }
     }
   }
@@ -4031,6 +4059,202 @@ void HandleInfoScreen(int mx, int my, int dx, int dy, int button)
 }
 
 
+// ============================================================================
+// change name functions
+// ============================================================================
+
+struct ApiRenamePlayerThreadData
+{
+  char *player_name;
+  char *player_uuid;
+};
+
+static void *CreateThreadData_ApiRenamePlayer(void)
+{
+  struct ApiRenamePlayerThreadData *data =
+    checked_malloc(sizeof(struct ApiRenamePlayerThreadData));
+
+  data->player_name = getStringCopy(setup.player_name);
+  data->player_uuid = getStringCopy(setup.player_uuid);
+
+  return data;
+}
+
+static void FreeThreadData_ApiRenamePlayer(void *data_raw)
+{
+  struct ApiRenamePlayerThreadData *data = data_raw;
+
+  checked_free(data->player_name);
+  checked_free(data->player_uuid);
+  checked_free(data);
+}
+
+static boolean SetRequest_ApiRenamePlayer(struct HttpRequest *request,
+                                         void *data_raw)
+{
+  struct ApiRenamePlayerThreadData *data = data_raw;
+  char *player_name_raw = data->player_name;
+  char *player_uuid_raw = data->player_uuid;
+
+  request->hostname = setup.api_server_hostname;
+  request->port     = API_SERVER_PORT;
+  request->method   = API_SERVER_METHOD;
+  request->uri      = API_SERVER_URI_RENAME;
+
+  char *player_name = getEscapedJSON(player_name_raw);
+  char *player_uuid = getEscapedJSON(player_uuid_raw);
+
+  snprintf(request->body, MAX_HTTP_BODY_SIZE,
+          "{\n"
+          "%s"
+          "  \"game_version\":         \"%s\",\n"
+          "  \"game_platform\":        \"%s\",\n"
+          "  \"name\":                 \"%s\",\n"
+          "  \"uuid\":                 \"%s\"\n"
+          "}\n",
+          getPasswordJSON(setup.api_server_password),
+          getProgramRealVersionString(),
+          getProgramPlatformString(),
+          player_name,
+          player_uuid);
+
+  checked_free(player_name);
+  checked_free(player_uuid);
+
+  ConvertHttpRequestBodyToServerEncoding(request);
+
+  return TRUE;
+}
+
+static void HandleResponse_ApiRenamePlayer(struct HttpResponse *response,
+                                          void *data_raw)
+{
+  // nothing to do here
+}
+
+#if defined(PLATFORM_EMSCRIPTEN)
+static void Emscripten_ApiRenamePlayer_Loaded(unsigned handle, void *data_raw,
+                                             void *buffer, unsigned int size)
+{
+  struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
+
+  if (response != NULL)
+  {
+    HandleResponse_ApiRenamePlayer(response, data_raw);
+
+    checked_free(response);
+  }
+  else
+  {
+    Error("server response too large to handle (%d bytes)", size);
+  }
+
+  FreeThreadData_ApiRenamePlayer(data_raw);
+}
+
+static void Emscripten_ApiRenamePlayer_Failed(unsigned handle, void *data_raw,
+                                             int code, const char *status)
+{
+  Error("server failed to handle request: %d %s", code, status);
+
+  FreeThreadData_ApiRenamePlayer(data_raw);
+}
+
+static void Emscripten_ApiRenamePlayer_Progress(unsigned handle, void *data_raw,
+                                               int bytes, int size)
+{
+  // nothing to do here
+}
+
+static void Emscripten_ApiRenamePlayer_HttpRequest(struct HttpRequest *request,
+                                                  void *data_raw)
+{
+  if (!SetRequest_ApiRenamePlayer(request, data_raw))
+  {
+    FreeThreadData_ApiRenamePlayer(data_raw);
+
+    return;
+  }
+
+  emscripten_async_wget2_data(request->uri,
+                             request->method,
+                             request->body,
+                             data_raw,
+                             TRUE,
+                             Emscripten_ApiRenamePlayer_Loaded,
+                             Emscripten_ApiRenamePlayer_Failed,
+                             Emscripten_ApiRenamePlayer_Progress);
+}
+
+#else
+
+static void ApiRenamePlayer_HttpRequestExt(struct HttpRequest *request,
+                                          struct HttpResponse *response,
+                                          void *data_raw)
+{
+  if (!SetRequest_ApiRenamePlayer(request, data_raw))
+    return;
+
+  if (!DoHttpRequest(request, response))
+  {
+    Error("HTTP request failed: %s", GetHttpError());
+
+    return;
+  }
+
+  if (!HTTP_SUCCESS(response->status_code))
+  {
+    Error("server failed to handle request: %d %s",
+         response->status_code,
+         response->status_text);
+
+    return;
+  }
+
+  HandleResponse_ApiRenamePlayer(response, data_raw);
+}
+
+static void ApiRenamePlayer_HttpRequest(struct HttpRequest *request,
+                                   struct HttpResponse *response,
+                                   void *data_raw)
+{
+  ApiRenamePlayer_HttpRequestExt(request, response, data_raw);
+
+  FreeThreadData_ApiRenamePlayer(data_raw);
+}
+#endif
+
+static int ApiRenamePlayerThread(void *data_raw)
+{
+  struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
+  struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
+
+  program.api_thread_count++;
+
+#if defined(PLATFORM_EMSCRIPTEN)
+  Emscripten_ApiRenamePlayer_HttpRequest(request, data_raw);
+#else
+  ApiRenamePlayer_HttpRequest(request, response, data_raw);
+#endif
+
+  program.api_thread_count--;
+
+  checked_free(request);
+  checked_free(response);
+
+  return 0;
+}
+
+static void ApiRenamePlayerAsThread(void)
+{
+  struct ApiRenamePlayerThreadData *data = CreateThreadData_ApiRenamePlayer();
+
+  ExecuteAsThread(ApiRenamePlayerThread,
+                 "ApiRenamePlayer", data,
+                 "rename player on server");
+}
+
+
 // ============================================================================
 // type name functions
 // ============================================================================
@@ -4153,10 +4377,17 @@ static void setTypeNameValues(char *name, struct TextPosInfo *pos,
     // temporarily change active user to edited user
     user.nr = type_name_nr;
 
-    // load setup of edited user (unless creating user with current setup)
-    if (!create_user ||
-       !Request("Use current setup values for the new player?", REQ_ASK))
+    if (create_user &&
+        Request("Use current setup values for the new player?", REQ_ASK))
+    {
+      // use current setup values for new user, but create new player UUID
+      setup.player_uuid = getStringCopy(getUUID());
+    }
+    else
+    {
+      // load setup for existing user (or start with defaults for new user)
       LoadSetup();
+    }
   }
 
   char *setup_filename = getSetupFilename();
@@ -4168,6 +4399,9 @@ static void setTypeNameValues(char *name, struct TextPosInfo *pos,
   // save setup of edited user
   SaveSetup();
 
+  // change name of edited user on score server
+  ApiRenamePlayerAsThread();
+
   if (game_status == GAME_MODE_PSEUDO_TYPENAMES || reset_setup)
   {
     if (reset_setup)
@@ -4586,7 +4820,8 @@ static void HandleChooseTree(int mx, int my, int dx, int dy, int button,
     }
     else if (game_status == GAME_MODE_SETUP)
     {
-      if (setup_mode == SETUP_MODE_CHOOSE_GAME_SPEED ||
+      if (setup_mode == SETUP_MODE_CHOOSE_SCORES_TYPE ||
+         setup_mode == SETUP_MODE_CHOOSE_GAME_SPEED ||
          setup_mode == SETUP_MODE_CHOOSE_SCROLL_DELAY ||
          setup_mode == SETUP_MODE_CHOOSE_SNAPSHOT_MODE)
        execSetupGame();
@@ -4753,7 +4988,8 @@ static void HandleChooseTree(int mx, int my, int dx, int dy, int button,
       {
        if (game_status == GAME_MODE_SETUP)
        {
-         if (setup_mode == SETUP_MODE_CHOOSE_GAME_SPEED ||
+         if (setup_mode == SETUP_MODE_CHOOSE_SCORES_TYPE ||
+             setup_mode == SETUP_MODE_CHOOSE_GAME_SPEED ||
              setup_mode == SETUP_MODE_CHOOSE_SCROLL_DELAY ||
              setup_mode == SETUP_MODE_CHOOSE_SNAPSHOT_MODE)
            execSetupGame();
@@ -4825,7 +5061,8 @@ static void HandleChooseTree(int mx, int my, int dx, int dy, int button,
 
        if (game_status == GAME_MODE_SETUP)
        {
-         if (setup_mode == SETUP_MODE_CHOOSE_GAME_SPEED ||
+         if (setup_mode == SETUP_MODE_CHOOSE_SCORES_TYPE ||
+             setup_mode == SETUP_MODE_CHOOSE_GAME_SPEED ||
              setup_mode == SETUP_MODE_CHOOSE_SCROLL_DELAY ||
              setup_mode == SETUP_MODE_CHOOSE_SNAPSHOT_MODE)
            execSetupGame();
@@ -5037,7 +5274,7 @@ void HandleChooseLevelNr(int mx, int my, int dx, int dy, int button)
   HandleChooseTree(mx, my, dx, dy, button, &level_number_current);
 }
 
-void DrawHallOfFame(int level_nr, int highlight_position)
+void DrawHallOfFame(int level_nr)
 {
   int fade_mask = REDRAW_FIELD;
 
@@ -5054,9 +5291,9 @@ void DrawHallOfFame(int level_nr, int highlight_position)
   SetDrawDeactivationMask(REDRAW_NONE);
   SetDrawBackgroundMask(REDRAW_FIELD);
 
-  if (highlight_position < 0) 
-    LoadScore(level_nr);
-  else
+  LoadLocalAndServerScore(level_nr, TRUE);
+
+  if (scores.last_added >= 0)
     SetAnimStatus(GAME_MODE_PSEUDO_SCORESNEW);
 
   FadeSetEnterScreen();
@@ -5070,15 +5307,47 @@ void DrawHallOfFame(int level_nr, int highlight_position)
 
   OpenDoor(GetDoorState() | DOOR_NO_DELAY | DOOR_FORCE_REDRAW);
 
-  HandleHallOfFame(level_nr, highlight_position, 0, 0, MB_MENU_INITIALIZE);
+  HandleHallOfFame(level_nr, 0, 0, 0, MB_MENU_INITIALIZE);
 
   DrawMaskedBorder(fade_mask);
 
   FadeIn(fade_mask);
 }
 
-static void drawHallOfFameList(int level_nr, int first_entry,
-                              int highlight_position)
+static int getHallOfFameFirstEntry(int first_entry, int step)
+{
+  if (step == 0)
+    first_entry = scores.last_added - (NUM_MENU_ENTRIES_ON_SCREEN + 1) / 2 + 1;
+  else
+    first_entry += step;
+
+  if (first_entry < 0)
+    first_entry = 0;
+  else if (first_entry > MAX_SCORE_ENTRIES - NUM_MENU_ENTRIES_ON_SCREEN)
+    first_entry = MAX(0, MAX_SCORE_ENTRIES - NUM_MENU_ENTRIES_ON_SCREEN);
+
+  return first_entry;
+}
+
+static char *getHallOfFameScoreText(int nr)
+{
+  if (!level.rate_time_over_score)
+    return int2str(scores.entry[nr].score, 5); // show normal score
+
+  if (level.use_step_counter)
+    return int2str(scores.entry[nr].time, 5);  // show number of steps
+
+  static char score_text[10];
+  int time_seconds = scores.entry[nr].time / FRAMES_PER_SECOND;
+  int mm = (time_seconds / 60) % 60;
+  int ss = (time_seconds % 60);
+
+  sprintf(score_text, "%02d:%02d", mm, ss);    // show playing time
+
+  return score_text;
+}
+
+static void drawHallOfFameList(int level_nr, int first_entry)
 {
   int i, j;
 
@@ -5092,7 +5361,8 @@ static void drawHallOfFameList(int level_nr, int first_entry,
   for (i = 0; i < NUM_MENU_ENTRIES_ON_SCREEN; i++)
   {
     int entry = first_entry + i;
-    boolean active = (entry == highlight_position);
+    boolean active = (entry == scores.last_added);
+    boolean forced = (scores.force_last_added && active);
     int font_nr1 = (active ? FONT_TEXT_1_ACTIVE : FONT_TEXT_1);
     int font_nr2 = (active ? FONT_TEXT_2_ACTIVE : FONT_TEXT_2);
     int font_nr3 = (active ? FONT_TEXT_3_ACTIVE : FONT_TEXT_3);
@@ -5103,17 +5373,18 @@ static void drawHallOfFameList(int level_nr, int first_entry,
     int dx3 = SXSIZE - 2 * (mSX - SX + dxoff) - 5 * getFontWidth(font_nr4);
     int num_dots = (dx3 - dx2) / getFontWidth(font_nr3);
     int sy = mSY + 64 + i * 32;
+    char *pos_text = (forced ? "???" : int2str(entry + 1, 3));
 
-    DrawText(mSX, sy, int2str(entry + 1, 3), font_nr1);
+    DrawText(mSX, sy, pos_text, font_nr1);
     DrawText(mSX + dx1, sy, ".", font_nr1);
 
     for (j = 0; j < num_dots; j++)
       DrawText(mSX + dx2 + j * getFontWidth(font_nr3), sy, ".", font_nr3);
 
-    if (!strEqual(highscore[entry].Name, EMPTY_PLAYER_NAME))
-      DrawText(mSX + dx2, sy, highscore[entry].Name, font_nr2);
+    if (!strEqual(scores.entry[entry].name, EMPTY_PLAYER_NAME))
+      DrawText(mSX + dx2, sy, scores.entry[entry].name, font_nr2);
 
-    DrawText(mSX + dx3, sy, int2str(highscore[entry].Score, 5), font_nr4);
+    DrawText(mSX + dx3, sy, getHallOfFameScoreText(entry), font_nr4);
   }
 
   redraw_mask |= REDRAW_FIELD;
@@ -5123,22 +5394,23 @@ void HandleHallOfFame(int mx, int my, int dx, int dy, int button)
 {
   static int level_nr = 0;
   static int first_entry = 0;
-  static int highlight_position = 0;
   int step = (button == 1 ? 1 : button == 2 ? 5 : 10);
 
   if (button == MB_MENU_INITIALIZE)
   {
     level_nr = mx;
-    highlight_position = my;
 
-    first_entry = highlight_position - (NUM_MENU_ENTRIES_ON_SCREEN + 1) / 2 + 1;
+    if (server_scores.updated)
+    {
+      // reload scores, using updated server score cache file
+      LoadLocalAndServerScore(level_nr, FALSE);
+
+      server_scores.updated = FALSE;
+    }
 
-    if (first_entry < 0)
-      first_entry = 0;
-    else if (first_entry + NUM_MENU_ENTRIES_ON_SCREEN > MAX_SCORE_ENTRIES)
-      first_entry = MAX(0, MAX_SCORE_ENTRIES - NUM_MENU_ENTRIES_ON_SCREEN);
+    first_entry = getHallOfFameFirstEntry(0, 0);
 
-    drawHallOfFameList(level_nr, first_entry, highlight_position);
+    drawHallOfFameList(level_nr, first_entry);
 
     return;
   }
@@ -5148,25 +5420,15 @@ void HandleHallOfFame(int mx, int my, int dx, int dy, int button)
 
   if (dy < 0)
   {
-    if (first_entry > 0)
-    {
-      first_entry -= step;
-      if (first_entry < 0)
-       first_entry = 0;
+    first_entry = getHallOfFameFirstEntry(first_entry, -step);
 
-      drawHallOfFameList(level_nr, first_entry, highlight_position);
-    }
+    drawHallOfFameList(level_nr, first_entry);
   }
   else if (dy > 0)
   {
-    if (first_entry + NUM_MENU_ENTRIES_ON_SCREEN < MAX_SCORE_ENTRIES)
-    {
-      first_entry += step;
-      if (first_entry + NUM_MENU_ENTRIES_ON_SCREEN > MAX_SCORE_ENTRIES)
-       first_entry = MAX(0, MAX_SCORE_ENTRIES - NUM_MENU_ENTRIES_ON_SCREEN);
+    first_entry = getHallOfFameFirstEntry(first_entry, step);
 
-      drawHallOfFameList(level_nr, first_entry, highlight_position);
-    }
+    drawHallOfFameList(level_nr, first_entry);
   }
   else if (button == MB_MENU_LEAVE || button == MB_MENU_CHOICE)
   {
@@ -5189,6 +5451,17 @@ void HandleHallOfFame(int mx, int my, int dx, int dy, int button)
       DrawMainMenu();
     }
   }
+  else if (server_scores.updated)
+  {
+    // reload scores, using updated server score cache file
+    LoadLocalAndServerScore(level_nr, FALSE);
+
+    server_scores.updated = FALSE;
+
+    first_entry = getHallOfFameFirstEntry(0, 0);
+
+    drawHallOfFameList(level_nr, first_entry);
+  }
 
   if (game_status == GAME_MODE_SCORES)
     PlayMenuSoundIfLoop();
@@ -5210,6 +5483,7 @@ static char *vsync_mode_text;
 static char *scroll_delay_text;
 static char *snapshot_mode_text;
 static char *game_speed_text;
+static char *scores_type_text;
 static char *network_server_text;
 static char *graphics_set_name;
 static char *sounds_set_name;
@@ -5230,6 +5504,56 @@ static void execSetupMain(void)
   DrawSetupScreen();
 }
 
+static void execSetupGame_setScoresType(void)
+{
+  if (scores_types == NULL)
+  {
+    int i;
+
+    for (i = 0; scores_types_list[i].value != NULL; i++)
+    {
+      TreeInfo *ti = newTreeInfo_setDefaults(TREE_TYPE_UNDEFINED);
+      char identifier[32], name[32];
+      char *value = scores_types_list[i].value;
+      char *text = scores_types_list[i].text;
+
+      ti->node_top = &scores_types;
+      ti->sort_priority = i;
+
+      sprintf(identifier, "%s", value);
+      sprintf(name, "%s", text);
+
+      setString(&ti->identifier, identifier);
+      setString(&ti->name, name);
+      setString(&ti->name_sorting, name);
+      setString(&ti->infotext, STR_SETUP_CHOOSE_SCORES_TYPE);
+
+      pushTreeInfo(&scores_types, ti);
+    }
+
+    // sort scores type values to start with lowest scores type value
+    sortTreeInfo(&scores_types);
+
+    // set current scores type value to configured scores type value
+    scores_type_current =
+      getTreeInfoFromIdentifier(scores_types, setup.scores_in_highscore_list);
+
+    // if that fails, set current scores type to reliable default value
+    if (scores_type_current == NULL)
+      scores_type_current =
+       getTreeInfoFromIdentifier(scores_types, STR_SCORES_TYPE_DEFAULT);
+
+    // if that also fails, set current scores type to first available value
+    if (scores_type_current == NULL)
+      scores_type_current = scores_types;
+  }
+
+  setup.scores_in_highscore_list = scores_type_current->identifier;
+
+  // needed for displaying scores type text instead of identifier
+  scores_type_text = scores_type_current->name;
+}
+
 static void execSetupGame_setGameSpeeds(boolean update_value)
 {
   if (setup.game_speed_extended)
@@ -5423,11 +5747,15 @@ static void execSetupGame(void)
   boolean check_vsync_mode = (setup_mode == SETUP_MODE_CHOOSE_GAME_SPEED);
 
   execSetupGame_setGameSpeeds(FALSE);
+  execSetupGame_setScoresType();
   execSetupGame_setScrollDelays();
   execSetupGame_setSnapshotModes();
 
   execSetupGame_setNetworkServerText();
 
+  if (!setup.provide_uploading_tapes)
+    setHideSetupEntry(execOfferUploadTapes);
+
   setup_mode = SETUP_MODE_GAME;
 
   DrawSetupScreen();
@@ -5437,6 +5765,13 @@ static void execSetupGame(void)
     DisableVsyncIfNeeded();
 }
 
+static void execSetupChooseScoresType(void)
+{
+  setup_mode = SETUP_MODE_CHOOSE_SCORES_TYPE;
+
+  DrawSetupScreen();
+}
+
 static void execSetupChooseGameSpeed(void)
 {
   setup_mode = SETUP_MODE_CHOOSE_GAME_SPEED;
@@ -6513,6 +6848,11 @@ static void execGadgetNetworkServer(void)
   ClickOnGadget(gi, MB_LEFTBUTTON);
 }
 
+static void execOfferUploadTapes(void)
+{
+  OfferUploadTapes();
+}
+
 static void ToggleNetworkModeIfNeeded(void)
 {
   int font_title = FONT_TITLE_1;
@@ -6618,6 +6958,9 @@ static struct
   void *related_value;
 } hide_related_entry_list[] =
 {
+  { &setup.scores_in_highscore_list,   execSetupChooseScoresType       },
+  { &setup.scores_in_highscore_list,   &scores_type_text               },
+
   { &setup.game_frame_delay,           execSetupChooseGameSpeed        },
   { &setup.game_frame_delay,           &game_speed_text                },
 
@@ -6730,6 +7073,10 @@ static struct TokenInfo setup_info_game[] =
   { TYPE_PLAYER,       &setup.network_player_nr,"Preferred Network Player:" },
   { TYPE_TEXT_INPUT,   execGadgetNetworkServer, "Network Server Hostname:" },
   { TYPE_STRING,       &network_server_text,   ""                      },
+  { TYPE_SWITCH,       &setup.use_api_server,  "Use Highscore Server:" },
+  { TYPE_ENTER_LIST,   execSetupChooseScoresType,"Scores in Highscore List:" },
+  { TYPE_STRING,       &scores_type_text,      ""                      },
+  { TYPE_ENTER_LIST,   execOfferUploadTapes,   "Upload Tapes to Server" },
   { TYPE_SWITCH,       &setup.multiple_users,  "Multiple Users/Teams:" },
   { TYPE_YES_NO,       &setup.input_on_focus,  "Only Move Focussed Player:" },
   { TYPE_SWITCH,       &setup.time_limit,      "Time Limit:"           },
@@ -6752,7 +7099,8 @@ static struct TokenInfo setup_info_game[] =
 #endif
   { TYPE_ENTER_LIST, execSetupChooseSnapshotMode,"Game Engine Snapshot Mode:" },
   { TYPE_STRING,       &snapshot_mode_text,    ""                      },
-  { TYPE_SWITCH,       &setup.show_snapshot_buttons,"Show Snapshot Buttons:" },
+  { TYPE_SWITCH,       &setup.show_load_save_buttons,"Show Load/Save Buttons:" },
+  { TYPE_SWITCH,       &setup.show_undo_redo_buttons,"Show Undo/Redo Buttons:" },
   { TYPE_EMPTY,                NULL,                   ""                      },
   { TYPE_LEAVE_MENU,   execSetupMain,          "Back"                  },
 
@@ -7331,6 +7679,10 @@ static void changeSetupValue(int screen_pos, int setup_info_pos_raw, int dx)
   if (si->value == &setup.network_mode)
     ToggleNetworkModeIfNeeded();
 
+  // API server mode may have changed at this point
+  if (si->value == &setup.use_api_server)
+    runtime.use_api_server = setup.use_api_server;
+
   // game speed list may have changed at this point
   if (si->value == &setup.game_speed_extended)
     ToggleGameSpeedsListIfNeeded();
@@ -8746,6 +9098,8 @@ void DrawSetupScreen(void)
 
   if (setup_mode == SETUP_MODE_INPUT)
     DrawSetupScreen_Input();
+  else if (setup_mode == SETUP_MODE_CHOOSE_SCORES_TYPE)
+    DrawChooseTree(&scores_type_current);
   else if (setup_mode == SETUP_MODE_CHOOSE_GAME_SPEED)
     DrawChooseTree(&game_speed_current);
   else if (setup_mode == SETUP_MODE_CHOOSE_SCROLL_DELAY)
@@ -8828,6 +9182,8 @@ void HandleSetupScreen(int mx, int my, int dx, int dy, int button)
 {
   if (setup_mode == SETUP_MODE_INPUT)
     HandleSetupScreen_Input(mx, my, dx, dy, button);
+  else if (setup_mode == SETUP_MODE_CHOOSE_SCORES_TYPE)
+    HandleChooseTree(mx, my, dx, dy, button, &scores_type_current);
   else if (setup_mode == SETUP_MODE_CHOOSE_GAME_SPEED)
     HandleChooseTree(mx, my, dx, dy, button, &game_speed_current);
   else if (setup_mode == SETUP_MODE_CHOOSE_SCROLL_DELAY)
@@ -8889,10 +9245,10 @@ void HandleGameActions(void)
   if (game_status != GAME_MODE_PLAYING)
     return;
 
-  GameActions();       // main game loop
+  GameActions();               // main game loop
 
   if (tape.auto_play && !tape.playing)
-    AutoPlayTapes();   // continue automatically playing next tape
+    AutoPlayTapesContinue();   // continue automatically playing next tape
 }
 
 
@@ -9683,3 +10039,117 @@ void DrawScreenAfterAddingSet(char *tree_subdir_new, int tree_type)
     }
   }
 }
+
+static int UploadTapes(void)
+{
+  SetGameStatus(GAME_MODE_LOADING);
+
+  FadeSetEnterScreen();
+  FadeOut(REDRAW_ALL);
+
+  ClearRectangle(drawto, 0, 0, WIN_XSIZE, WIN_YSIZE);
+
+  FadeIn(REDRAW_ALL);
+
+  DrawInitTextHead("Uploading tapes");
+
+  global.autoplay_mode = AUTOPLAY_MODE_UPLOAD;
+  global.autoplay_leveldir = "ALL";
+  global.autoplay_all = TRUE;
+
+  int num_tapes_uploaded = AutoPlayTapes();
+
+  global.autoplay_mode = AUTOPLAY_MODE_NONE;
+  global.autoplay_leveldir = NULL;
+  global.autoplay_all = FALSE;
+
+  SetGameStatus(GAME_MODE_MAIN);
+
+  DrawMainMenu();
+
+  return num_tapes_uploaded;
+}
+
+static boolean OfferUploadTapes(void)
+{
+  if (!Request(setup.has_remaining_tapes ?
+              "Upload missing tapes to the high score server now?" :
+              "Upload all your tapes to the high score server now?", REQ_ASK))
+    return FALSE;
+
+  int num_tapes_uploaded = UploadTapes();
+  char message[100];
+
+  if (num_tapes_uploaded < 0)
+  {
+    num_tapes_uploaded = -num_tapes_uploaded - 1;
+
+    if (num_tapes_uploaded == 0)
+      sprintf(message, "Upload failed! No tapes uploaded!");
+    else if (num_tapes_uploaded == 1)
+      sprintf(message, "Upload failed! Only 1 tape uploaded!");
+    else
+      sprintf(message, "Upload failed! Only %d tapes uploaded!",
+             num_tapes_uploaded);
+
+    Request(message, REQ_CONFIRM);
+
+    // if uploading tapes failed, add tape upload entry to setup menu
+    setup.provide_uploading_tapes = TRUE;
+    setup.has_remaining_tapes = TRUE;
+
+    SaveSetup_ServerSetup();
+
+    return FALSE;
+  }
+
+  if (num_tapes_uploaded == 0)
+    sprintf(message, "No tapes uploaded!");
+  else if (num_tapes_uploaded == 1)
+    sprintf(message, "1 tape uploaded!");
+  else
+    sprintf(message, "%d tapes uploaded!", num_tapes_uploaded);
+
+  Request(message, REQ_CONFIRM);
+
+  if (num_tapes_uploaded > 0)
+    Request("New scores will be visible after a few minutes!", REQ_CONFIRM);
+
+  // after all tapes have been uploaded, remove entry from setup menu
+  setup.provide_uploading_tapes = FALSE;
+  setup.has_remaining_tapes = FALSE;
+
+  SaveSetup_ServerSetup();
+
+  return TRUE;
+}
+
+void CheckUploadTapes(void)
+{
+  if (!setup.ask_for_uploading_tapes)
+    return;
+
+  // after asking for uploading tapes, do not ask again
+  setup.ask_for_uploading_tapes = FALSE;
+  setup.ask_for_remaining_tapes = FALSE;
+
+  if (directoryExists(getTapeDir(NULL)))
+  {
+    boolean tapes_uploaded = OfferUploadTapes();
+
+    if (!tapes_uploaded)
+    {
+      Request(setup.has_remaining_tapes ?
+             "You can upload missing tapes from the setup menu later!" :
+             "You can upload your tapes from the setup menu later!",
+             REQ_CONFIRM);
+    }
+  }
+  else
+  {
+    // if tapes directory does not exist yet, never offer uploading all tapes
+    setup.provide_uploading_tapes = FALSE;
+  }
+
+  SaveSetup_ServerSetup();
+}
index d79745d..3d5a131 100644 (file)
@@ -22,7 +22,7 @@
 void DrawMainMenuExt(int);
 void DrawAndFadeInMainMenu(int);
 void DrawMainMenu(void);
-void DrawHallOfFame(int, int);
+void DrawHallOfFame(int);
 void DrawScreenAfterAddingSet(char *, int);
 
 void RedrawSetupScreenAfterFullscreenToggle(void);
@@ -47,4 +47,6 @@ void setHideRelatedSetupEntries(void);
 void DumpScreenIdentifiers(void);
 boolean DoScreenAction(int);
 
+void CheckUploadTapes(void);
+
 #endif // SCREENS_H
index c1e4bce..2a1a3ec 100644 (file)
@@ -312,7 +312,8 @@ static void DrawVideoDisplay_DateTime(unsigned int state, unsigned int value)
       char s[MAX_DATETIME_STRING_SIZE];
       int year2 = value / 10000;
       int year4 = (year2 < 70 ? 2000 + year2 : 1900 + year2);
-      int month_index = (value / 100) % 100;
+      int month_index_raw = (value / 100) % 100;
+      int month_index = month_index_raw % 12;  // prevent invalid index
       int month = month_index + 1;
       int day = value % 100;
 
@@ -470,8 +471,44 @@ void TapeDeactivateDisplayOff(boolean redraw_display)
 // tape logging functions
 // ============================================================================
 
+struct AutoPlayInfo
+{
+  LevelDirTree *leveldir;
+  boolean all_levelsets;
+  int last_level_nr;
+  int level_nr;
+  int num_levels_played;
+  int num_levels_solved;
+  int num_tapes_patched;
+  int num_tape_missing;
+  boolean level_failed[MAX_TAPES_PER_SET];
+  char *tape_filename;
+};
+
 static char tape_patch_info[MAX_OUTPUT_LINESIZE];
 
+static void PrintTapeReplayHeader(struct AutoPlayInfo *autoplay)
+{
+  PrintLine("=", 79);
+
+  if (global.autoplay_mode == AUTOPLAY_MODE_FIX)
+    Print("Automatically fixing level tapes\n");
+  else if (global.autoplay_mode == AUTOPLAY_MODE_UPLOAD)
+    Print("Automatically uploading level tapes\n");
+  else
+    Print("Automatically playing level tapes\n");
+
+  PrintLine("-", 79);
+  Print("Level series identifier: '%s'\n", autoplay->leveldir->identifier);
+  Print("Level series name:       '%s'\n", autoplay->leveldir->name);
+  Print("Level series author:     '%s'\n", autoplay->leveldir->author);
+  Print("Number of levels:        %d\n",   autoplay->leveldir->levels);
+  PrintLine("=", 79);
+  Print("\n");
+
+  DrawInitTextItem(autoplay->leveldir->name);
+}
+
 static void PrintTapeReplayProgress(boolean replay_finished)
 {
   static unsigned int counter_last = -1;
@@ -512,6 +549,79 @@ static void PrintTapeReplayProgress(boolean replay_finished)
   }
 }
 
+static void PrintTapeReplaySummary(struct AutoPlayInfo *autoplay)
+{
+  char *autoplay_status =
+    (autoplay->num_levels_played == autoplay->num_levels_solved &&
+     autoplay->num_levels_played > 0 ? " OK " : "WARN");
+  int autoplay_percent =
+    (autoplay->num_levels_played ?
+     autoplay->num_levels_solved * 100 / autoplay->num_levels_played : 0);
+  int i;
+
+  Print("\n");
+  PrintLine("=", 79);
+  Print("Number of levels played: %d\n", autoplay->num_levels_played);
+  Print("Number of levels solved: %d (%d%%)\n", autoplay->num_levels_solved,
+       (autoplay->num_levels_played ?
+        autoplay->num_levels_solved * 100 / autoplay->num_levels_played : 0));
+  if (global.autoplay_mode == AUTOPLAY_MODE_FIX)
+    Print("Number of tapes fixed: %d\n", autoplay->num_tapes_patched);
+  PrintLine("-", 79);
+  Print("Summary (for automatic parsing by scripts):\n");
+
+  if (autoplay->tape_filename)
+  {
+    Print("TAPEFILE [%s] '%s', %d, %d, %d",
+         autoplay_status,
+         autoplay->leveldir->identifier,
+         autoplay->last_level_nr,
+         game.score_final,
+         game.score_time_final);
+  }
+  else
+  {
+    Print("LEVELDIR [%s] '%s', SOLVED %d/%d (%d%%)",
+         autoplay_status,
+         autoplay->leveldir->identifier,
+         autoplay->num_levels_solved,
+         autoplay->num_levels_played,
+         autoplay_percent);
+
+    if (autoplay->num_levels_played != autoplay->num_levels_solved)
+    {
+      Print(", FAILED:");
+      for (i = 0; i < MAX_TAPES_PER_SET; i++)
+       if (autoplay->level_failed[i])
+         Print(" %03d", i);
+    }
+  }
+
+  Print("\n");
+  PrintLine("=", 79);
+}
+
+static FILE *tape_log_file;
+
+static void OpenTapeLogfile(void)
+{
+  if (!(tape_log_file = fopen(options.tape_log_filename, MODE_WRITE)))
+    Warn("cannot write tape logfile '%s'", options.tape_log_filename);
+}
+
+static void WriteTapeLogfile(byte action[MAX_TAPE_ACTIONS])
+{
+  int i;
+
+  for (i = 0; i < MAX_TAPE_ACTIONS; i++)
+    putFile8Bit(tape_log_file, action[i]);
+}
+
+static void CloseTapeLogfile(void)
+{
+  fclose(tape_log_file);
+}
+
 
 // ============================================================================
 // tape control functions
@@ -540,6 +650,8 @@ void TapeErase(void)
   tape.length_frames = 0;
   tape.length_seconds = 0;
 
+  tape.score_tape_basename[0] = '\0';
+
   if (leveldir_current)
   {
     strncpy(tape.level_identifier, leveldir_current->identifier,
@@ -557,6 +669,8 @@ void TapeErase(void)
   tape.game_version = GAME_VERSION_ACTUAL;
   tape.engine_version = level.game_version;
 
+  tape.property_bits = TAPE_PROPERTY_NONE;
+
   TapeSetDateFromNow();
 
   for (i = 0; i < MAX_PLAYERS; i++)
@@ -648,6 +762,8 @@ static void TapeAppendRecording(void)
   // set current delay (for last played move)
   tape.pos[tape.counter].delay = tape.delay_played;
 
+  tape.property_bits |= TAPE_PROPERTY_REPLAYED;
+
   // set current date
   TapeSetDateFromNow();
 
@@ -738,6 +854,12 @@ void TapeRecordAction(byte action_raw[MAX_TAPE_ACTIONS])
     tape.set_centered_player = FALSE;
   }
 
+  if (GameFrameDelay != GAME_FRAME_DELAY)
+    tape.property_bits |= TAPE_PROPERTY_GAME_SPEED;
+
+  if (setup.small_game_graphics || SCR_FIELDX >= 2 * SCR_FIELDX_DEFAULT)
+    tape.property_bits |= TAPE_PROPERTY_SMALL_GRAPHICS;
+
   if (!TapeAddAction(action))
     TapeStopRecording();
 }
@@ -759,6 +881,12 @@ void TapeTogglePause(boolean toggle_mode)
   if (tape.single_step && (toggle_mode & TAPE_TOGGLE_MANUAL))
     tape.single_step = FALSE;
 
+  if (tape.single_step)
+    tape.property_bits |= TAPE_PROPERTY_SINGLE_STEP;
+
+  if (tape.pausing)
+    tape.property_bits |= TAPE_PROPERTY_PAUSE_MODE;
+
   DrawVideoDisplayCurrentState();
 
   if (tape.deactivate_display)
@@ -788,12 +916,14 @@ void TapeTogglePause(boolean toggle_mode)
 
   if (game_status == GAME_MODE_PLAYING)
   {
-    if (setup.show_snapshot_buttons && CheckEngineSnapshotList())
+    if (setup.show_load_save_buttons &&
+       setup.show_undo_redo_buttons &&
+       CheckEngineSnapshotList())
     {
       if (tape.pausing)
        MapUndoRedoButtons();
       else if (!tape.single_step)
-       UnmapUndoRedoButtons();
+       MapLoadSaveButtons();
     }
 
     ModifyPauseButtons();
@@ -934,6 +1064,9 @@ byte *TapePlayAction(void)
   if (tape.auto_play)
     PrintTapeReplayProgress(FALSE);
 
+  if (options.tape_log_filename != NULL)
+    WriteTapeLogfile(action);
+
   return action;
 }
 
@@ -1041,6 +1174,8 @@ void TapeQuickSave(void)
     return;
   }
 
+  tape.property_bits |= TAPE_PROPERTY_SNAPSHOT;
+
   if (SaveTapeChecked(tape.level_nr))
     SaveEngineSnapshotSingle();
 }
@@ -1099,6 +1234,7 @@ void TapeQuickLoad(void)
     TapeStartWarpForward(AUTOPLAY_MODE_WARP_NO_DISPLAY);
 
     tape.quick_resume = TRUE;
+    tape.property_bits |= TAPE_PROPERTY_SNAPSHOT;
   }
   else // this should not happen (basically checked above)
   {
@@ -1223,16 +1359,83 @@ void FixTape_ForceSinglePlayer(void)
 // tape autoplay functions
 // ----------------------------------------------------------------------------
 
-void AutoPlayTapes(void)
+static TreeInfo *getNextValidAutoPlayEntry(TreeInfo *node)
 {
-  static LevelDirTree *autoplay_leveldir = NULL;
-  static boolean autoplay_initialized = FALSE;
-  static int autoplay_level_nr = -1;
-  static int num_levels_played = 0;
-  static int num_levels_solved = 0;
-  static int num_tapes_patched = 0;
-  static int num_tape_missing = 0;
-  static boolean level_failed[MAX_TAPES_PER_SET];
+  node = getNextValidTreeInfoEntry(node);
+
+  while (node && node->is_copy)
+    node = getNextValidTreeInfoEntry(node);
+
+  return node;
+}
+
+static TreeInfo *getFirstValidAutoPlayEntry(TreeInfo *node)
+{
+  node = getFirstValidTreeInfoEntry(node);
+
+  if (node && node->is_copy)
+    return getNextValidAutoPlayEntry(node);
+
+  return node;
+}
+
+static void AutoPlayTapes_SetScoreEntry(int score, int time)
+{
+  // set unique basename for score tape (for uploading to score server)
+  strcpy(tape.score_tape_basename, getScoreTapeBasename(setup.player_name));
+
+  // store score in first score entry
+  scores.last_added = 0;
+
+  struct ScoreEntry *entry = &scores.entry[scores.last_added];
+
+  strncpy(entry->tape_basename, tape.score_tape_basename, MAX_FILENAME_LEN);
+  strncpy(entry->name, setup.player_name, MAX_PLAYER_NAME_LEN);
+
+  entry->score = score;
+  entry->time = time;
+
+  PrintNoLog("- uploading score tape to score server ... ");
+
+  server_scores.uploaded = FALSE;
+}
+
+static boolean AutoPlayTapes_WaitForUpload(void)
+{
+  unsigned int upload_delay = 0;
+  unsigned int upload_delay_value = 10000;
+
+  ResetDelayCounter(&upload_delay);
+
+  // wait for score tape to be successfully uploaded (and fail on timeout)
+  while (!server_scores.uploaded)
+  {
+    if (DelayReached(&upload_delay, upload_delay_value))
+    {
+      PrintNoLog("\r");
+      Print("- uploading score tape to score server - TIMEOUT.\n");
+
+      if (program.headless)
+       Fail("cannot upload score tape to score server");
+
+      return FALSE;
+    }
+
+    UPDATE_BUSY_STATE();
+
+    Delay(20);
+  }
+
+  PrintNoLog("\r");
+  Print("- uploading score tape to score server - uploaded.\n");
+
+  return TRUE;
+}
+
+static int AutoPlayTapesExt(boolean initialize)
+{
+  static struct AutoPlayInfo autoplay;
+  static int num_tapes = 0;
   static int patch_nr = 0;
   static char *patch_name[] =
   {
@@ -1266,9 +1469,12 @@ void AutoPlayTapes(void)
 
     -1
   };
+  LevelDirTree *leveldir_current_last = leveldir_current;
+  boolean init_level_set = FALSE;
+  int level_nr_last = level_nr;
   int i;
 
-  if (autoplay_initialized)
+  if (!initialize)
   {
     if (global.autoplay_mode == AUTOPLAY_MODE_FIX)
     {
@@ -1287,7 +1493,7 @@ void AutoPlayTapes(void)
          SaveTapeToFilename(filename);
 
          tape.auto_play_level_fixed = TRUE;
-         num_tapes_patched++;
+         autoplay.num_tapes_patched++;
        }
 
        // continue with next tape
@@ -1309,67 +1515,221 @@ void AutoPlayTapes(void)
     // just finished auto-playing tape
     PrintTapeReplayProgress(TRUE);
 
+    if (options.tape_log_filename != NULL)
+      CloseTapeLogfile();
+
+    if (global.autoplay_mode == AUTOPLAY_MODE_SAVE &&
+       tape.auto_play_level_solved)
+    {
+      AutoPlayTapes_SetScoreEntry(game.score_final, game.score_time_final);
+
+      if (leveldir_current)
+      {
+       // the tape's level set identifier may differ from current level set
+       strncpy(tape.level_identifier, leveldir_current->identifier,
+               MAX_FILENAME_LEN);
+       tape.level_identifier[MAX_FILENAME_LEN] = '\0';
+
+       // the tape's level number may differ from current level number
+       tape.level_nr = level_nr;
+      }
+
+      // save score tape to upload to server; may be required for some reasons:
+      // * level set identifier in solution tapes may differ from level set
+      // * level set identifier is missing (old-style tape without INFO chunk)
+      // * solution tape may have native format (like Supaplex solution files)
+
+      SaveScoreTape(level_nr);
+      SaveServerScore(level_nr, TRUE);
+
+      AutoPlayTapes_WaitForUpload();
+    }
+
     if (patch_nr == 0)
-      num_levels_played++;
+      autoplay.num_levels_played++;
 
     if (tape.auto_play_level_solved)
-      num_levels_solved++;
+      autoplay.num_levels_solved++;
 
     if (level_nr >= 0 && level_nr < MAX_TAPES_PER_SET)
-      level_failed[level_nr] = !tape.auto_play_level_solved;
+      autoplay.level_failed[level_nr] = !tape.auto_play_level_solved;
   }
   else
   {
-    DrawCompleteVideoDisplay();
+    if (strEqual(global.autoplay_leveldir, "ALL"))
+    {
+      autoplay.all_levelsets = TRUE;
 
-    audio.sound_enabled = FALSE;
-    setup.engine_snapshot_mode = getStringCopy(STR_SNAPSHOT_MODE_OFF);
+      // tape mass-uploading only allowed for private tapes
+      if (global.autoplay_mode == AUTOPLAY_MODE_UPLOAD)
+       options.mytapes = TRUE;
+    }
 
-    autoplay_leveldir = getTreeInfoFromIdentifier(leveldir_first,
-                                                 global.autoplay_leveldir);
+    if ((global.autoplay_mode == AUTOPLAY_MODE_SAVE ||
+        global.autoplay_mode == AUTOPLAY_MODE_UPLOAD) &&
+       !options.mytapes &&
+       options.player_name == NULL)
+    {
+      Fail("specify player name when uploading solution tapes");
+    }
 
-    if (autoplay_leveldir == NULL)
-      Fail("no such level identifier: '%s'", global.autoplay_leveldir);
+    if (global.autoplay_mode != AUTOPLAY_MODE_UPLOAD)
+      DrawCompleteVideoDisplay();
 
-    leveldir_current = autoplay_leveldir;
+    if (program.headless)
+    {
+      audio.sound_enabled = FALSE;
+      setup.engine_snapshot_mode = getStringCopy(STR_SNAPSHOT_MODE_OFF);
+    }
 
-    if (autoplay_leveldir->first_level < 0)
-      autoplay_leveldir->first_level = 0;
-    if (autoplay_leveldir->last_level >= MAX_TAPES_PER_SET)
-      autoplay_leveldir->last_level = MAX_TAPES_PER_SET - 1;
+    if (strSuffix(global.autoplay_leveldir, ".tape"))
+    {
+      autoplay.tape_filename = global.autoplay_leveldir;
 
-    autoplay_level_nr = autoplay_leveldir->first_level;
+      if (!fileExists(autoplay.tape_filename))
+       Fail("tape file '%s' does not exist", autoplay.tape_filename);
 
-    PrintLine("=", 79);
-    if (global.autoplay_mode == AUTOPLAY_MODE_FIX)
-      Print("Automatically fixing level tapes\n");
+      LoadTapeFromFilename(autoplay.tape_filename);
+
+      if (tape.no_valid_file)
+       Fail("cannot load tape file '%s'", autoplay.tape_filename);
+
+      if (tape.no_info_chunk && !options.identifier)
+       Fail("cannot get levelset from tape file '%s'", autoplay.tape_filename);
+
+      if (tape.no_info_chunk && !options.level_nr)
+       Fail("cannot get level nr from tape file '%s'", autoplay.tape_filename);
+
+      global.autoplay_leveldir = tape.level_identifier;
+
+      if (options.identifier != NULL)
+       global.autoplay_leveldir = options.identifier;
+
+      if (options.level_nr != NULL)
+       tape.level_nr = atoi(options.level_nr);
+
+      if (tape.level_nr >= 0 && tape.level_nr < MAX_TAPES_PER_SET)
+        global.autoplay_level[tape.level_nr] = TRUE;
+
+      global.autoplay_all = FALSE;
+      options.mytapes = FALSE;
+    }
+
+    if (autoplay.all_levelsets)
+    {
+      // start auto-playing first level set
+      autoplay.leveldir = getFirstValidAutoPlayEntry(leveldir_first);
+    }
     else
-      Print("Automatically playing level tapes\n");
-    PrintLine("-", 79);
-    Print("Level series identifier: '%s'\n", autoplay_leveldir->identifier);
-    Print("Level series name:       '%s'\n", autoplay_leveldir->name);
-    Print("Level series author:     '%s'\n", autoplay_leveldir->author);
-    Print("Number of levels:        %d\n",   autoplay_leveldir->levels);
-    PrintLine("=", 79);
-    Print("\n");
+    {
+      // auto-play selected level set
+      autoplay.leveldir = getTreeInfoFromIdentifier(leveldir_first,
+                                                   global.autoplay_leveldir);
+    }
 
-    for (i = 0; i < MAX_TAPES_PER_SET; i++)
-      level_failed[i] = FALSE;
+    if (autoplay.leveldir == NULL)
+      Fail("no such level identifier: '%s'", global.autoplay_leveldir);
 
     // only private tapes may be modified
     if (global.autoplay_mode == AUTOPLAY_MODE_FIX)
       options.mytapes = TRUE;
 
-    autoplay_initialized = TRUE;
+    // set timestamp for batch tape upload
+    global.autoplay_time = time(NULL);
+
+    num_tapes = 0;
+
+    init_level_set = TRUE;
   }
 
   while (1)
   {
+    if (init_level_set)
+    {
+      leveldir_current = autoplay.leveldir;
+
+      if (autoplay.leveldir->first_level < 0)
+       autoplay.leveldir->first_level = 0;
+      if (autoplay.leveldir->last_level >= MAX_TAPES_PER_SET)
+       autoplay.leveldir->last_level = MAX_TAPES_PER_SET - 1;
+
+      autoplay.level_nr = autoplay.leveldir->first_level;
+
+      autoplay.num_levels_played = 0;
+      autoplay.num_levels_solved = 0;
+      autoplay.num_tapes_patched = 0;
+      autoplay.num_tape_missing = 0;
+
+      for (i = 0; i < MAX_TAPES_PER_SET; i++)
+       autoplay.level_failed[i] = FALSE;
+
+      PrintTapeReplayHeader(&autoplay);
+
+      init_level_set = FALSE;
+    }
+
+    if (autoplay.all_levelsets && global.autoplay_mode == AUTOPLAY_MODE_UPLOAD)
+    {
+      boolean skip_levelset = FALSE;
+
+      if (!directoryExists(getTapeDir(autoplay.leveldir->subdir)))
+      {
+       Print("No tape directory for this level set found -- skipping.\n");
+
+       skip_levelset = TRUE;
+      }
+
+      if (CheckTapeDirectoryUploadsComplete(autoplay.leveldir->subdir))
+      {
+       Print("All tapes for this level set already uploaded -- skipping.\n");
+
+       skip_levelset = TRUE;
+      }
+
+      if (skip_levelset)
+      {
+       PrintTapeReplaySummary(&autoplay);
+
+       // continue with next level set
+       autoplay.leveldir = getNextValidAutoPlayEntry(autoplay.leveldir);
+
+       // all level sets processed
+       if (autoplay.leveldir == NULL)
+         break;
+
+       init_level_set = TRUE;
+
+       continue;
+      }
+    }
+
     if (global.autoplay_mode != AUTOPLAY_MODE_FIX || patch_nr == 0)
-      level_nr = autoplay_level_nr++;
+      level_nr = autoplay.level_nr++;
 
-    if (level_nr > autoplay_leveldir->last_level)
-      break;
+    UPDATE_BUSY_STATE();
+
+    // check if all tapes for this level set have been processed
+    if (level_nr > autoplay.leveldir->last_level)
+    {
+      PrintTapeReplaySummary(&autoplay);
+
+      if (!autoplay.all_levelsets)
+       break;
+
+      if (global.autoplay_mode == AUTOPLAY_MODE_UPLOAD)
+       MarkTapeDirectoryUploadsAsComplete(autoplay.leveldir->subdir);
+
+      // continue with next level set
+      autoplay.leveldir = getNextValidAutoPlayEntry(autoplay.leveldir);
+
+      // all level sets processed
+      if (autoplay.leveldir == NULL)
+       break;
+
+      init_level_set = TRUE;
+
+      continue;
+    }
 
     // set patch info (required for progress output)
     strcpy(tape_patch_info, "");
@@ -1380,7 +1740,18 @@ void AutoPlayTapes(void)
     if (!global.autoplay_all && !global.autoplay_level[level_nr])
       continue;
 
+    // speed things up in case of missing private tapes (skip loading level)
+    if (options.mytapes && !fileExists(getTapeFilename(level_nr)))
+    {
+      autoplay.num_tape_missing++;
+
+      Print("Tape %03d: (no tape found)\n", level_nr);
+
+      continue;
+    }
+
     TapeErase();
+    TapeRewind();      // needed to reset "tape.auto_play_level_solved"
 
     LoadLevel(level_nr);
 
@@ -1397,14 +1768,16 @@ void AutoPlayTapes(void)
     continue;
 #endif
 
-    if (options.mytapes)
+    if (autoplay.tape_filename)
+      LoadTapeFromFilename(autoplay.tape_filename);
+    else if (options.mytapes)
       LoadTape(level_nr);
     else
       LoadSolutionTape(level_nr);
 
     if (tape.no_valid_file)
     {
-      num_tape_missing++;
+      autoplay.num_tape_missing++;
 
       Print("Tape %03d: (no tape found)\n", level_nr);
 
@@ -1468,40 +1841,100 @@ void AutoPlayTapes(void)
       }
     }
 
+    num_tapes++;
+
+    if (global.autoplay_mode == AUTOPLAY_MODE_UPLOAD)
+    {
+      boolean use_temporary_tape_file = FALSE;
+
+      Print("Tape %03d:\n", level_nr);
+
+      AutoPlayTapes_SetScoreEntry(0, 0);
+
+      if (autoplay.tape_filename == NULL)
+      {
+       autoplay.tape_filename = (options.mytapes ? getTapeFilename(level_nr) :
+                                 getDefaultSolutionTapeFilename(level_nr));
+
+       if (!fileExists(autoplay.tape_filename))
+       {
+         // non-standard or incorrect solution tape -- save to temporary file
+         autoplay.tape_filename = getTemporaryTapeFilename();
+
+         SaveTapeToFilename(autoplay.tape_filename);
+
+         use_temporary_tape_file = TRUE;
+       }
+      }
+
+      SaveServerScoreFromFile(level_nr, TRUE, autoplay.tape_filename);
+
+      boolean success = AutoPlayTapes_WaitForUpload();
+
+      if (use_temporary_tape_file)
+        unlink(autoplay.tape_filename);
+
+      // required for uploading multiple tapes
+      autoplay.tape_filename = NULL;
+
+      if (!success)
+      {
+       num_tapes = -num_tapes;
+
+       break;
+      }
+
+      continue;
+    }
+
     InitCounter();
 
+    if (options.tape_log_filename != NULL)
+      OpenTapeLogfile();
+
     TapeStartGamePlaying();
     TapeStartWarpForward(global.autoplay_mode);
 
-    return;
-  }
+    autoplay.last_level_nr = level_nr;
 
-  Print("\n");
-  PrintLine("=", 79);
-  Print("Number of levels played: %d\n", num_levels_played);
-  Print("Number of levels solved: %d (%d%%)\n", num_levels_solved,
-       (num_levels_played ? num_levels_solved * 100 / num_levels_played : 0));
-  if (global.autoplay_mode == AUTOPLAY_MODE_FIX)
-    Print("Number of tapes fixed: %d\n", num_tapes_patched);
-  PrintLine("-", 79);
-  Print("Summary (for automatic parsing by scripts):\n");
-  Print("LEVELDIR [%s] '%s', SOLVED %d/%d (%d%%)",
-       (num_levels_played == num_levels_solved ? " OK " : "WARN"),
-       autoplay_leveldir->identifier, num_levels_solved, num_levels_played,
-       (num_levels_played ? num_levels_solved * 100 / num_levels_played : 0));
+    return num_tapes;
+  }
 
-  if (num_levels_played != num_levels_solved)
+  if (global.autoplay_mode == AUTOPLAY_MODE_UPLOAD)
   {
-    Print(", FAILED:");
-    for (i = 0; i < MAX_TAPES_PER_SET; i++)
-      if (level_failed[i])
-       Print(" %03d", i);
+    Print("\n");
+    PrintLine("=", 79);
+
+    if (num_tapes >= 0)
+      Print("SUMMARY: %d tapes uploaded.\n", num_tapes);
+    else
+      Print("SUMMARY: Uploading tapes failed.\n");
+
+    PrintLine("=", 79);
   }
 
-  Print("\n");
-  PrintLine("=", 79);
+  // clear timestamp for batch tape upload (required after interactive upload)
+  global.autoplay_time = 0;
 
-  CloseAllAndExit(0);
+  // exit if running headless or if visually auto-playing tapes
+  if (program.headless || global.autoplay_mode != AUTOPLAY_MODE_UPLOAD)
+    CloseAllAndExit(0);
+
+  // when running interactively, restore last selected level set and number
+  leveldir_current = leveldir_current_last;
+  level_nr = level_nr_last;
+
+  return num_tapes;
+}
+
+int AutoPlayTapes(void)
+{
+  return AutoPlayTapesExt(TRUE);
+}
+
+int AutoPlayTapesContinue(void)
+{
+  return AutoPlayTapesExt(FALSE);
 }
 
 
index a829448..4b914c7 100644 (file)
 // values for tape properties stored in tape file
 #define TAPE_PROPERTY_NONE             0
 #define TAPE_PROPERTY_EM_RANDOM_BUG    (1 << 0)
+#define TAPE_PROPERTY_GAME_SPEED       (1 << 1)
+#define TAPE_PROPERTY_PAUSE_MODE       (1 << 2)
+#define TAPE_PROPERTY_SINGLE_STEP      (1 << 3)
+#define TAPE_PROPERTY_SNAPSHOT         (1 << 4)
+#define TAPE_PROPERTY_REPLAYED         (1 << 5)
+#define TAPE_PROPERTY_TAS_KEYS         (1 << 6)
+#define TAPE_PROPERTY_SMALL_GRAPHICS   (1 << 7)
+
+// values for score tape basename length (date, time, name hash, no extension)
+#define MAX_SCORE_TAPE_BASENAME_LEN    24
 
 // some positions in the video tape control window
 #define VIDEO_DISPLAY1_XPOS    5
@@ -178,6 +188,7 @@ struct TapeInfo
   int game_version;    // game release version the tape was created with
   int engine_version;  // game engine version the tape was recorded with
 
+  char score_tape_basename[MAX_FILENAME_LEN + 1];
   char level_identifier[MAX_FILENAME_LEN + 1];
   int level_nr;
   unsigned int random_seed;
@@ -228,6 +239,7 @@ struct TapeInfo
 
   boolean show_game_buttons;   // show game buttons in tape viewport
 
+  boolean no_info_chunk;       // used to identify old tape file format
   boolean no_valid_file;       // set when tape file missing or invalid
 };
 
@@ -264,7 +276,8 @@ boolean PlaySolutionTape(void);
 void UndoTape(void);
 void FixTape_ForceSinglePlayer(void);
 
-void AutoPlayTapes(void);
+int AutoPlayTapes(void);
+int AutoPlayTapesContinue(void);
 void PatchTapes(void);
 
 void CreateTapeButtons(void);
index b0df344..c859e63 100644 (file)
@@ -1416,39 +1416,42 @@ void SetBorderElement(void)
   }
 }
 
-void FloodFillLevelExt(int from_x, int from_y, int fill_element,
+void FloodFillLevelExt(int start_x, int start_y, int fill_element,
                       int max_array_fieldx, int max_array_fieldy,
                       short field[max_array_fieldx][max_array_fieldy],
                       int max_fieldx, int max_fieldy)
 {
-  int i,x,y;
-  int old_element;
-  static int check[4][2] = { { -1, 0 }, { 0, -1 }, { 1, 0 }, { 0, 1 } };
-  static int safety = 0;
+  static struct XY stack_buffer[MAX_LEV_FIELDX * MAX_LEV_FIELDY];
+  static struct XY check[4] = { { -1, 0 }, { 0, -1&