Repository: fabiangreffrath/woof
Pull request #2046: add a "prev map" key binding
Author: login: fabiangreffrath
Created: 2024-11-23T22:48:07Z | Merged: 2025-05-08T05:06:04Z

Fixes #2007

Linked issue #2007: Add a "prev map" key binding
This would be very useful for testing. I think the key bindings menu is out of room though.
Linked issue comments: user: login: fabiangreffrath
created_at: 2024-11-22T13:00:34Z
body: How about this?

```C
int G_GotoPrevLevel(void)
{
    const int cur_epsd = gameepisode;
    const int cur_map = gamemap;
    const int start_epsd = (gamemode == commercial) ? 1 : 9;
    int ret = false;

    for (gameepisode = start_epsd; gameepisode > 0; gameepisode--)
    {
        for (gamemap = 99; gamemap >= 0; gamemap--)
        {
            int next_epsd, next_map;
            G_GotoNextLevel(&next_epsd, &next_map);

            if (next_epsd == cur_epsd && next_map == cur_map)
            {
                G_DeferedInitNew(gameskill, gameepisode, gamemap);
                ret = true;
                break;
            }
        }
    }

    gameepisode = cur_epsd;
    gamemap = cur_map;

    return ret;
}
```

author_association: OWNER
user: login: rfomin
created_at: 2024-11-22T13:31:40Z
body: > for (gamemap = 99; gamemap >= 0; gamemap--)

Why are we looking from the end?

BTW, It is possible that we will not find the current map due to secret exits. Try to test ["Fork in the Road" WAD](https://www.doomworld.com/forum/topic/107191-fork-in-the-road-a-umapinfo-project-2019-07-03-update-alpha-3/)
author_association: COLLABORATOR
user: login: fabiangreffrath
created_at: 2024-11-22T13:52:14Z
body: > Why are we looking from the end?

I'd like to find the highest possible map that would lead to the current map. For reasons I don't remember. It appeared plausible to me. 😁 

> BTW, It is possible that we will not find the current map due to secret exits. Try to test ["Fork in the Road" WAD](https://www.doomworld.com/forum/topic/107191-fork-in-the-road-a-umapinfo-project-2019-07-03-update-alpha-3/)

Yes, it's possible, but then nothing will happen. We will have to provide feedback to the player, just as we do for e.g. IDCLEV99.
author_association: OWNER

Diff:
diff --git a/src/g_game.c b/src/g_game.c
index cbad5cabf..b4dd7b4ba 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -1076,12 +1076,12 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
     {12, 13, 19, 15, 16, 17, 18, 21, 14},
     {22, 23, 24, 25, 29, 27, 28, 31, 26},
     {32, 33, 34, 35, 36, 39, 38, 41, 37},
-    {42, 49, 44, 45, 46, 47, 48, 11, 43}
+    {42, 49, 44, 45, 46, 47, 48, -1, 43}
   };
   byte doom2_next[32] = {
      2,  3,  4,  5,  6,  7,  8,  9, 10, 11,
     12, 13, 14, 15, 31, 17, 18, 19, 20, 21,
-    22, 23, 24, 25, 26, 27, 28, 29, 30,  1,
+    22, 23, 24, 25, 26, 27, 28, 29, 30, -1,
     32, 16
   };
 
@@ -1096,25 +1096,19 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
       next = gamemapinfo->nextsecret;
     else if (gamemapinfo->nextmap[0])
       next = gamemapinfo->nextmap;
-    else if (gamemapinfo->flags & MapInfo_EndGame)
-    {
-      epsd = 1;
-      map = 1;
-    }
 
     if (next)
       G_ValidateMapName(next, &epsd, &map);
   }
-
-  if (map == -1)
+  else
   {
     // secret level
     doom2_next[14] = (haswolflevels ? 31 : 16);
 
     // shareware doom has only episode 1
-    doom_next[0][7] = (gamemode == shareware ? 11 : 21);
+    doom_next[0][7] = (gamemode == shareware ? -1 : 21);
 
-    doom_next[2][7] = (gamemode == registered ? 11 : 41);
+    doom_next[2][7] = (gamemode == registered ? -1 : 41);
 
     //doom2_next and doom_next are 0 based, unlike gameepisode and gamemap
     epsd = gameepisode - 1;
@@ -1159,8 +1153,11 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   {
     char *name = MapName(epsd, map);
 
-    if (W_CheckNumForName(name) == -1)
-      displaymsg("Next level not found: %s", name);
+    if (map == -1 || W_CheckNumForName(name) == -1)
+    {
+      name = MapName(gameepisode, gamemap);
+      displaymsg("Next level not found for %s", name);
+    }
     else
     {
       G_DeferedInitNew(gameskill, epsd, map);
@@ -1171,6 +1168,66 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    struct mapentry_s *const cur_gamemapinfo = gamemapinfo;
+    int ret = false;
+
+    do
+    {
+        gamemap = cur_map;
+
+        while ((gamemap = (gamemap + 99) % 100) != cur_map)
+        {
+            int next_epsd, next_map;
+            gamemapinfo = G_LookupMapinfo(gameepisode, gamemap);
+            G_GotoNextLevel(&next_epsd, &next_map);
+
+            // do not let linear and UMAPINFO maps cross
+            if ((cur_gamemapinfo == NULL && gamemapinfo != NULL) ||
+                (cur_gamemapinfo != NULL && gamemapinfo == NULL))
+            {
+                continue;
+            }
+
+            if (next_epsd == cur_epsd && next_map == cur_map)
+            {
+                char *name = MapName(gameepisode, gamemap);
+
+                if (W_CheckNumForName(name) != -1)
+                {
+                    G_DeferedInitNew(gameskill, gameepisode, gamemap);
+                    ret = true;
+                    break;
+                }
+            }
+        }
+    } while (ret == false
+             // only check one episode in Doom 2
+             && gamemode != commercial
+             && (gameepisode = (gameepisode + 9) % 10) != cur_epsd);
+
+    gameepisode = cur_epsd;
+    gamemap = cur_map;
+    gamemapinfo = cur_gamemapinfo;
+
+    if (ret == false)
+    {
+        char *name = MapName(gameepisode, gamemap);
+        displaymsg("Previous level not found for %s", name);
+    }
+
+    return ret;
+}
+
 static boolean G_StrictModeSkipEvent(event_t *ev)
 {
   static boolean enable_mouse = false;
diff --git a/src/g_game.h b/src/g_game.h
index 896367196..60ca3d793 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -82,6 +82,7 @@ demo_version_t G_GetNamedComplevel(const char *arg);
 const char *G_GetCurrentComplevelName(void);
 
 int G_GotoNextLevel(int *pEpi, int *pMap);
+int G_GotoPrevLevel(void);
 
 void G_BindGameInputVariables(void);
 void G_BindGameVariables(void);
diff --git a/src/m_input.c b/src/m_input.c
index f5e449829..8a1ab1a93 100644
--- a/src/m_input.c
+++ b/src/m_input.c
@@ -702,6 +702,7 @@ void M_BindInputVariables(void)
 
     BIND_INPUT(input_menu_reloadlevel, "Restart current level/demo");
     BIND_INPUT(input_menu_nextlevel, "Go to next level");
+    BIND_INPUT(input_menu_prevlevel, "Go to previous level");
 
     BIND_INPUT(input_hud_timestats, "Toggle display of level stats and time");
 
diff --git a/src/m_input.h b/src/m_input.h
index c135ce1b5..265a8e492 100644
--- a/src/m_input.h
+++ b/src/m_input.h
@@ -68,6 +68,7 @@ enum
     input_menu_clear,
     input_menu_reloadlevel,
     input_menu_nextlevel,
+    input_menu_prevlevel,
 
     input_hud_timestats,
 
diff --git a/src/mn_menu.c b/src/mn_menu.c
index 3ab9c0338..dc1f51b26 100644
--- a/src/mn_menu.c
+++ b/src/mn_menu.c
@@ -2627,6 +2627,18 @@ boolean M_ShortcutResponder(const event_t *ev)
         }
     }
 
+    if (M_InputActivated(input_menu_prevlevel))
+    {
+        if (demoplayback && singledemo && !PLAYBACK_SKIP)
+        {
+            return false;
+        }
+        else if (G_GotoPrevLevel())
+        {
+            return true;
+        }
+    }
+
     if (M_InputActivated(input_demo_fforward))
     {
         if (demoplayback && !PLAYBACK_SKIP && !fastdemo && !D_CheckNetConnect())
diff --git a/src/mn_setup.c b/src/mn_setup.c
index 66188bf14..66cf47684 100644
--- a/src/mn_setup.c
+++ b/src/mn_setup.c
@@ -1418,6 +1418,7 @@ static setup_menu_t keys_settings3[] = {
     // [FG] reload current level / go to next level
     {"Reload Map/Demo", S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_menu_reloadlevel},
     {"Next Map",        S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_menu_nextlevel},
+    {"Previous Map",    S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_menu_prevlevel},
     {"Show Stats/Time", S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_hud_timestats},
     MI_GAP,
     {"Fast-FWD Demo",   S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_demo_fforward},
@@ -1428,10 +1429,6 @@ static setup_menu_t keys_settings3[] = {
     {"Default Speed",   S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_speed_default},
     MI_GAP,
     {"Begin Chat",      S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_chat},
-    {"Player 1",        S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_chat_dest0},
-    {"Player 2",        S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_chat_dest1},
-    {"Player 3",        S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_chat_dest2},
-    {"Player 4",        S_INPUT, KB_X, M_SPC, {0}, m_scrn, input_chat_dest3},
     MI_END
 };
 


Reviews:
user: login: fabiangreffrath
state: COMMENTED
author_association: OWNER
submitted_at: 2024-11-23T22:48:40Z
user: login: rfomin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2024-11-24T13:17:37Z
user: login: rfomin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2024-11-24T13:26:30Z
user: login: fabiangreffrath
state: COMMENTED
author_association: OWNER
submitted_at: 2024-11-25T07:53:27Z
user: login: fabiangreffrath
state: COMMENTED
author_association: OWNER
submitted_at: 2024-11-25T07:53:54Z
user: login: rfomin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2024-11-25T10:16:08Z
user: login: ceski-1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2024-12-04T03:11:28Z
user: login: rfomin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2024-12-04T05:28:53Z
user: login: fabiangreffrath
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-29T08:15:55Z
user: login: rfomin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T09:53:58Z
user: login: rfomin
body: LGTM, thanks.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-04T10:04:58Z
user: login: ceski-1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T03:42:40Z
user: login: fabiangreffrath
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-06T06:13:25Z
user: login: ceski-1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T22:32:49Z
user: login: rfomin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:10:47Z
user: login: fabiangreffrath
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T07:20:54Z
user: login: rfomin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T08:11:56Z
user: login: fabiangreffrath
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T10:43:21Z
user: login: rfomin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T10:56:02Z
user: login: ceski-1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T23:12:32Z
user: login: ceski-1
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-07T23:12:42Z

Review comments:
diff_hunk: @@ -2608,6 +2608,18 @@ boolean M_ShortcutResponder(const event_t *ev)
         }
     }
 
+    if (M_InputActivated(input_menu_prevlevel))
+    {
+        if (demoplayback && singledemo && !PLAYBACK_SKIP)
path: src/mn_menu.c
user: login: fabiangreffrath
body: Not sure about this part.
created_at: 2024-11-23T22:48:40Z
author_association: OWNER
diff_hunk: @@ -702,6 +702,7 @@ void M_BindInputVariables(void)
 
     BIND_INPUT(input_menu_reloadlevel, "Restart current level/demo");
     BIND_INPUT(input_menu_nextlevel, "Go to next level");
+    BIND_INPUT(input_menu_prevlevel, "Go to previous level");
path: src/m_input.c
user: login: rfomin
body: It's config only? We don't have room for it in the "Key Bindings" menu. Maybe we should move player 1-4 to config:
![image](https://github.com/user-attachments/assets/5aa0a964-fba1-46f7-a87b-dfa1b65f197d)

created_at: 2024-11-24T13:17:37Z
author_association: COLLABORATOR
diff_hunk: @@ -2608,6 +2608,18 @@ boolean M_ShortcutResponder(const event_t *ev)
         }
     }
 
+    if (M_InputActivated(input_menu_prevlevel))
+    {
+        if (demoplayback && singledemo && !PLAYBACK_SKIP)
path: src/mn_menu.c
user: login: rfomin
body: "Next Level" works during `demoplayback`. "Prev Level" would not be so practical - we would have to restart the demo from the beginning. I think we need demo compatible save/rewind to implement the "Prev Level" feature well.
created_at: 2024-11-24T13:26:30Z
author_association: COLLABORATOR
diff_hunk: @@ -2608,6 +2608,18 @@ boolean M_ShortcutResponder(const event_t *ev)
         }
     }
 
+    if (M_InputActivated(input_menu_prevlevel))
+    {
+        if (demoplayback && singledemo && !PLAYBACK_SKIP)
path: src/mn_menu.c
user: login: fabiangreffrath
body: > I think we need demo compatible save/rewind to implement the "Prev Level" feature well.

For demos, yes. But for regular play?

What if you start the game with `-warp 1 2` and hit the "prev level" button? I would expect it to go back to E1M1, although this map has not been visited in the current run.
created_at: 2024-11-25T07:53:27Z
author_association: OWNER
diff_hunk: @@ -702,6 +702,7 @@ void M_BindInputVariables(void)
 
     BIND_INPUT(input_menu_reloadlevel, "Restart current level/demo");
     BIND_INPUT(input_menu_nextlevel, "Go to next level");
+    BIND_INPUT(input_menu_prevlevel, "Go to previous level");
path: src/m_input.c
user: login: fabiangreffrath
body: Yes, that's a good solution.
created_at: 2024-11-25T07:53:54Z
author_association: OWNER
diff_hunk: @@ -2608,6 +2608,18 @@ boolean M_ShortcutResponder(const event_t *ev)
         }
     }
 
+    if (M_InputActivated(input_menu_prevlevel))
+    {
+        if (demoplayback && singledemo && !PLAYBACK_SKIP)
path: src/mn_menu.c
user: login: rfomin
body: > For demos, yes. But for regular play?

Yes, I was talking about demos. Sure, we can implement "prev map" without demo playback support. 
created_at: 2024-11-25T10:16:08Z
author_association: COLLABORATOR
diff_hunk: @@ -2608,6 +2608,18 @@ boolean M_ShortcutResponder(const event_t *ev)
         }
     }
 
+    if (M_InputActivated(input_menu_prevlevel))
+    {
+        if (demoplayback && singledemo && !PLAYBACK_SKIP)
path: src/mn_menu.c
user: login: ceski-1
body: Demo support for "prev level" would be very useful.
created_at: 2024-12-04T03:11:28Z
author_association: COLLABORATOR
diff_hunk: @@ -2608,6 +2608,18 @@ boolean M_ShortcutResponder(const event_t *ev)
         }
     }
 
+    if (M_InputActivated(input_menu_prevlevel))
+    {
+        if (demoplayback && singledemo && !PLAYBACK_SKIP)
path: src/mn_menu.c
user: login: rfomin
body: > Demo support for "prev level" would be very useful.

Then it will take a while until we have demo-compatible saves. DSDA-Doom has an implementation of it, but I want to try to extend zone memory with "relative pointers" idea: https://nullprogram.com/blog/2023/09/30/
> instead of pointers I use signed integers whose value indicates an offset relative to itself

This would allow us to save/restore the entire playsim with a single `memcpy`. Not sure if this will work though.

created_at: 2024-12-04T05:28:53Z
author_association: COLLABORATOR
diff_hunk: @@ -2608,6 +2608,18 @@ boolean M_ShortcutResponder(const event_t *ev)
         }
     }
 
+    if (M_InputActivated(input_menu_prevlevel))
+    {
+        if (demoplayback && singledemo && !PLAYBACK_SKIP)
path: src/mn_menu.c
user: login: fabiangreffrath
body: > > Demo support for "prev level" would be very useful.
> 
> Then it will take a while until we have demo-compatible saves.

ALternatively, we could just start demo playback anew and fast-forward to the level determined as "prev map"?
created_at: 2025-04-29T08:15:54Z
author_association: OWNER
diff_hunk: @@ -2608,6 +2608,18 @@ boolean M_ShortcutResponder(const event_t *ev)
         }
     }
 
+    if (M_InputActivated(input_menu_prevlevel))
+    {
+        if (demoplayback && singledemo && !PLAYBACK_SKIP)
path: src/mn_menu.c
user: login: rfomin
body: > ALternatively, we could just start demo playback anew and fast-forward to the level determined as "prev map"?

This won't work for very long demos, where the prev map key is most useful. Imagine if you want to jump from level 26 to level 25 - you will have to wait fast-forward of 25 levels, which can take some time in intense WADs.

Alternatively, I suggest "prev map" without demo support for now.
created_at: 2025-04-29T09:53:58Z
author_association: COLLABORATOR
diff_hunk: @@ -1171,6 +1171,58 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    int ret = false;
+
+    do
path: src/g_game.c
user: login: ceski-1
body: It looks like there are issues with Legacy of Rust (both next and prev directions).
created_at: 2025-05-06T03:42:40Z
author_association: COLLABORATOR
diff_hunk: @@ -1171,6 +1171,58 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    int ret = false;
+
+    do
path: src/g_game.c
user: login: fabiangreffrath
body: The latest commit should fix this.

I couldn't spot a problem with the next key, though. If you mean that each map that ends the game continues at E1M1/MAP01, that's intended.
created_at: 2025-05-06T06:13:25Z
author_association: OWNER
diff_hunk: @@ -1171,6 +1171,58 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    int ret = false;
+
+    do
path: src/g_game.c
user: login: ceski-1
body: Here's the current behavior:

Start first episode:
- Press "next map": repeats e1m1-e1m7 forever
- Press "prev map": initially e1m1, then e2m7-e2m1, then "previous level not found for map08" (stays on e2m1)

Start second episode:
- Press "next map": e2m1-e2m7, then repeats e1m1-e1m7 forever
- Press "prev map": initially e2m1, then "previous level not found for map08" (stays on e2m1)

Is that intended? It seems kind of odd.
created_at: 2025-05-06T22:32:49Z
author_association: COLLABORATOR
diff_hunk: @@ -1171,6 +1171,58 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    int ret = false;
+
+    do
path: src/g_game.c
user: login: rfomin
body: I confirm that Legacy of Rust works strangely. I suggest to remove "cycle" - when user presses "prev" on first map, last map is not searched. Episode concept in UMAPINFO makes it overcomplicated and confusing for user. I think we should remove "cycle" from "next map" as well, for consistency.
created_at: 2025-05-07T02:10:47Z
author_association: COLLABORATOR
diff_hunk: @@ -1171,6 +1171,58 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    int ret = false;
+
+    do
path: src/g_game.c
user: login: fabiangreffrath
body: Alright, so should I modify the code to remove cycling from prev/next key and let the sequence end at maps that end the game?
created_at: 2025-05-07T07:20:54Z
author_association: OWNER
diff_hunk: @@ -1171,6 +1171,58 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    int ret = false;
+
+    do
path: src/g_game.c
user: login: rfomin
body: > Alright, so should I modify the code to remove cycling from prev/next key and let the sequence end at maps that end the game?

Yes, I think it will be right.
created_at: 2025-05-07T08:11:56Z
author_association: COLLABORATOR
diff_hunk: @@ -1171,6 +1171,58 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    int ret = false;
+
+    do
path: src/g_game.c
user: login: fabiangreffrath
body: > Yes, I think it will be right.

done
created_at: 2025-05-07T10:43:21Z
author_association: OWNER
diff_hunk: @@ -1171,6 +1171,58 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    int ret = false;
+
+    do
path: src/g_game.c
user: login: rfomin
body: Legacy of Rust E2M7 (MAP14) will jump to E1M0 (MAP15) when user presses "next map" and endless cycle continues. This is probably specific to Legacy of Rust, but I suggest just stopping "next map" if UMAPINFO doesn't have a `next` field.
created_at: 2025-05-07T10:56:02Z
author_association: COLLABORATOR
diff_hunk: @@ -1171,6 +1171,58 @@ int G_GotoNextLevel(int *pEpi, int *pMap)
   return false;
 }
 
+int G_GotoPrevLevel(void)
+{
+    if (gamestate != GS_LEVEL || deathmatch || netgame || demorecording
+        || demoplayback || menuactive)
+    {
+        return false;
+    }
+
+    const int cur_epsd = gameepisode;
+    const int cur_map = gamemap;
+    int ret = false;
+
+    do
path: src/g_game.c
user: login: ceski-1
body: Everything works nicely now.
created_at: 2025-05-07T23:12:32Z
author_association: COLLABORATOR

PR comments:
user: login: fabiangreffrath
created_at: 2025-05-01T17:16:40Z
body: Ready for review!
author_association: OWNER
user: login: fabiangreffrath
created_at: 2025-05-01T17:23:36Z
body: How would you like if this worked like the `|<<` button on the cd player, i.e. go back to the beginning of the map if pressed once and go back to the previous level of pressed again within 1 second?
author_association: OWNER
user: login: rfomin
created_at: 2025-05-02T00:45:46Z
body: > How would you like if this worked like the `|<<` button on the cd player, i.e. go back to the beginning of the map if pressed once and go back to the previous level of pressed again within 1 second?

No, I never liked the way CD players worked. The music app players I use just switch tracks instantly. I haven't used a physical music player in over 20 years, and I think it would be confusing to most people.
author_association: COLLABORATOR
user: login: rfomin
created_at: 2025-05-02T02:09:02Z
body: It seems that this PR does not work with UMAPINFO. I have tested it with Eviternity II.
author_association: COLLABORATOR
user: login: fabiangreffrath
created_at: 2025-05-02T05:02:04Z
body: > No, I never liked the way CD players worked. The music app players I use just switch tracks instantly. I haven't used a physical music player in over 20 years, and I think it would be confusing to most people.

Alright, it was just an idea. :wink: 
author_association: OWNER
user: login: rfomin
created_at: 2025-05-07T14:03:27Z
body: Now it shows MAP-1 for the end of UMAPINFO episode:
![image](https://github.com/user-attachments/assets/365b299e-4a8a-4405-b6c8-6de85e869cd5)

author_association: COLLABORATOR
user: login: fabiangreffrath
created_at: 2025-05-07T15:07:22Z
body: > Now it shows MAP-1 for the end of UMAPINFO episode

Mostly cosmetic issue. We shouldn't report the map we couldn't find, but the map that we couldn't find a previous map for.
author_association: OWNER