Quake 2 is another classic from id software.While not modded quite as much as it's predecessor or Q3A, it is still a blast to play and the mod's on offer are bundles of fun. Just step into Quake 2 Rocket Arena, armed with the new and almighty rocket and rail gun and you will be gibbing for hours on end.If you dig fast paced action, then load up the good ole Quake 2, fire up a mod or two and have some fun!

Post tutorial Report RSS Implementing the PMenu

Hello! Paril here, yet again with a vengence for modifications! Anyways, here's a tutorial for how to add Zoid's CTF PMenu system into your everyday normal Deathmatch, Teamplay, CTB, Pong Deathmatch or whatever mod! It's a bit hard, but we&

Posted by on - Advanced Client Side Coding

[page=Introduction]

Adding the PMenu system is a basic/intermediate thing to do. Learning how to use it can get difficult, because of the way PMenu uses it's menus. Nevertheless, I will teach you the basics and the way around of this very good menu system!

Now, let's get started. I will go file by file, so it shouldn't be too much of a challenge.

[page=p_menu.c]

Alright, create that file and add it to your project. I'll give you the whole file here, since explaining it takes a long time, you don't really need to know much about anything in p_menu.c, except how to open the menu and close the menu. Here's the file:

/*
Copyright (C) 1997-2001 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
#include "g_local.h"

// Note that the pmenu entries are duplicated
// this is so that a static set of pmenu entries can be used
// for multiple clients and changed without interference
// note that arg will be freed when the menu is closed, it must be allocated memory
pmenuhnd_t *PMenu_Open(edict_t *ent, pmenu_t *entries, int cur, int num, void *arg)
{
	pmenuhnd_t *hnd;
	pmenu_t *p;
	int i;

	if (!ent->client)
		return NULL;

	if (ent->client->menu) {
		gi.dprintf("warning, ent already has a menu\n");
		PMenu_Close(ent);
	}

	hnd = malloc(sizeof(*hnd));

	hnd->arg = arg;
	hnd->entries = malloc(sizeof(pmenu_t) * num);
	memcpy(hnd->entries, entries, sizeof(pmenu_t) * num);
	// duplicate the strings since they may be from static memory
	for (i = 0; i < num; i++)
		if (entries[i].text)
			hnd->entries[i].text = strdup(entries[i].text);

	hnd->num = num;

	if (cur < 0 || !entries[cur].SelectFunc) {
		for (i = 0, p = entries; i < num; i++, p++)
			if (p->SelectFunc)
				break;
	} else
		i = cur;

	if (i >= num)
		hnd->cur = -1;
	else
		hnd->cur = i;

	ent->client->showscores = true;
	ent->client->inmenu = true;
	ent->client->menu = hnd;

	PMenu_Do_Update(ent);
	gi.unicast (ent, true);

	return hnd;
}

void PMenu_Close(edict_t *ent)
{
	int i;
	pmenuhnd_t *hnd;

	if (!ent->client->menu)
		return;

	hnd = ent->client->menu;
	for (i = 0; i < hnd->num; i++)
		if (hnd->entries[i].text)
			free(hnd->entries[i].text);
	free(hnd->entries);
	if (hnd->arg)
		free(hnd->arg);
	free(hnd);
	ent->client->menu = NULL;
	ent->client->showscores = false;
}

// only use on pmenu's that have been called with PMenu_Open
void PMenu_UpdateEntry(pmenu_t *entry, const char *text, int align, SelectFunc_t SelectFunc)
{
	if (entry->text)
		free(entry->text);
	entry->text = strdup(text);
	entry->align = align;
	entry->SelectFunc = SelectFunc;
}

void PMenu_Do_Update(edict_t *ent)
{
	char string[1400];
	int i;
	pmenu_t *p;
	int x;
	pmenuhnd_t *hnd;
	char *t;
	qboolean alt = false;

	if (!ent->client->menu) {
		gi.dprintf("warning:  ent has no menu\n");
		return;
	}

	hnd = ent->client->menu;

	strcpy(string, "xv 32 yv 8 picn inventory ");

	for (i = 0, p = hnd->entries; i < hnd->num; i++, p++) {
		if (!p->text || !*(p->text))
			continue; // blank line
		t = p->text;
		if (*t == '*') {
			alt = true;
			t++;
		}
		sprintf(string + strlen(string), "yv %d ", 32 + i * 8);
		if (p->align == PMENU_ALIGN_CENTER)
			x = 196/2 - strlen(t)*4 + 64;
		else if (p->align == PMENU_ALIGN_RIGHT)
			x = 64 + (196 - strlen(t)*8);
		else
			x = 64;

		sprintf(string + strlen(string), "xv %d ",
			x - ((hnd->cur == i) ? 8 : 0));

		if (hnd->cur == i)
			sprintf(string + strlen(string), "string2 \"\x0d%s\" ", t);
		else if (alt)
			sprintf(string + strlen(string), "string2 \"%s\" ", t);
		else
			sprintf(string + strlen(string), "string \"%s\" ", t);
		alt = false;
	}

	gi.WriteByte (svc_layout);
	gi.WriteString (string);
}

void PMenu_Update(edict_t *ent)
{
	if (!ent->client->menu) {
		gi.dprintf("warning:  ent has no menu\n");
		return;
	}

	if (level.time - ent->client->menutime >= 1.0) {
		// been a second or more since last update, update now
		PMenu_Do_Update(ent);
		gi.unicast (ent, true);
		ent->client->menutime = level.time;
		ent->client->menudirty = false;
	}
	ent->client->menutime = level.time + 0.2;
	ent->client->menudirty = true;
}

void PMenu_Next(edict_t *ent)
{
	pmenuhnd_t *hnd;
	int i;
	pmenu_t *p;

	if (!ent->client->menu) {
		gi.dprintf("warning:  ent has no menu\n");
		return;
	}

	hnd = ent->client->menu;

	if (hnd->cur < 0)
		return; // no selectable entries

	i = hnd->cur;
	p = hnd->entries + hnd->cur;
	do {
		i++, p++;
		if (i == hnd->num)
			i = 0, p = hnd->entries;
		if (p->SelectFunc)
			break;
	} while (i != hnd->cur);

	hnd->cur = i;

	PMenu_Update(ent);
}

void PMenu_Prev(edict_t *ent)
{
	pmenuhnd_t *hnd;
	int i;
	pmenu_t *p;

	if (!ent->client->menu) {
		gi.dprintf("warning:  ent has no menu\n");
		return;
	}

	hnd = ent->client->menu;

	if (hnd->cur < 0)
		return; // no selectable entries

	i = hnd->cur;
	p = hnd->entries + hnd->cur;
	do {
		if (i == 0) {
			i = hnd->num - 1;
			p = hnd->entries + i;
		} else
			i--, p--;
		if (p->SelectFunc)
			break;
	} while (i != hnd->cur);

	hnd->cur = i;

	PMenu_Update(ent);
}

void PMenu_Select(edict_t *ent)
{
	pmenuhnd_t *hnd;
	pmenu_t *p;

	if (!ent->client->menu) {
		gi.dprintf("warning:  ent has no menu\n");
		return;
	}

	hnd = ent->client->menu;

	if (hnd->cur < 0)
		return; // no selectable entries

	p = hnd->entries + hnd->cur;

	if (p->SelectFunc)
		p->SelectFunc(ent, hnd);
}

Whoo! Alot of code to shake off our heads. Read through it if you have a taste for it. spacing might be a problem, but it still works.

Alright, let's go to our main header file..

[page=p_menu.h]

This is the main header file that we will later include in g_local.h (do it now if you wish, but I will remind you). Here it is.

/*
Copyright (C) 1997-2001 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

enum {
	PMENU_ALIGN_LEFT,
	PMENU_ALIGN_CENTER,
	PMENU_ALIGN_RIGHT
};

typedef struct pmenuhnd_s {
	struct pmenu_s *entries;
	int cur;
	int num;
	void *arg;
} pmenuhnd_t;

typedef void (*SelectFunc_t)(edict_t *ent, pmenuhnd_t *hnd);

typedef struct pmenu_s {
	char *text;
	int align;
	SelectFunc_t SelectFunc;
} pmenu_t;

pmenuhnd_t *PMenu_Open(edict_t *ent, pmenu_t *entries, int cur, int num, void *arg);
void PMenu_Close(edict_t *ent);
void PMenu_UpdateEntry(pmenu_t *entry, const char *text, int align, SelectFunc_t SelectFunc);
void PMenu_Do_Update(edict_t *ent);
void PMenu_Update(edict_t *ent);
void PMenu_Next(edict_t *ent);
void PMenu_Prev(edict_t *ent);
void PMenu_Select(edict_t *ent);

Heh. Big files, but it's worth it. Alright, now we have to work on G_local itself. It shouldn't be much of a hard feat getting what we need in.

[page=G_local.h]

Let's go to our gclient_t structure, and add the following to the end of the structure so it looks something like...

float		flood_when[10];		// when messages were said
	int			flood_whenhead;		// head pointer for when said

	float		respawn_time;		// can respawn when time > this
//ZOID
	qboolean	inmenu;				// in menu
	pmenuhnd_t	*menu;				// current menu	float		menutime;			// time to update menu
	qboolean	menudirty;
//ZOID
};

As I said before, we have to include p_menu.h, or we will get many errors. Include it after g_game, so like:

#include "p_menu.h"

or whatever you named it out of a random event. Now to edit some of the client-side stuff.

[page=p_view.c]

Head down to the bottom of ClientEndServerFrame, and find:

// if the scoreboard is up, update it
	if (ent->client->showscores && !(level.framenum & 31) )
	{
		DeathmatchScoreboardMessage (ent, ent->enemy);
		gi.unicast (ent, false);
	}

Change it to this:

// if the scoreboard is up, update it
	if (ent->client->showscores && !(level.framenum & 31) )
	{
//ZOID
		if (ent->client->menu) {
			PMenu_Do_Update(ent);
			ent->client->menudirty = false;
			ent->client->menutime = level.time;
		} else
//ZOID
		DeathmatchScoreboardMessage (ent, ent->enemy);
		gi.unicast (ent, false);
	}

That is so the menu will update if it's up.

[page=p_client.c]

Head down to ClientThink way at the bottom, and add this in:

//ZOID
	for (i = 1; i <= maxclients->value; i++) {
		other = g_edicts + i;
		if (other->inuse && other->client->chase_target == ent)
			UpdateChaseCam(other);
	}

	if (client->menudirty && client->menutime <= level.time) {
		PMenu_Do_Update(ent);
		gi.unicast (ent, true);
		client->menutime = level.time;
		client->menudirty = false;
	}
//ZOID

Again with the updates.

[page=g_cmds.c]

The last part of adding stuff to the core code.

Go to cmd_inven_f, and add this after cl = ent->client;

//ZOID
	if (ent->client->menu) {
		PMenu_Close(ent);
		ent->client->update_chase = true;
		return;
	}
//ZOID

Now to SelectNextItem.

Replace the first few 3 or 4 chasecam lines after cl = ent->client with this:

//ZOID
	if (cl->menu) {
		PMenu_Next(ent);
		return;
	} else if (cl->chase_target) {
		ChaseNext(ent);
		return;
	}
//ZOID

Same with SelectPrevItem, but this time:

//ZOID
	if (cl->menu) {
		PMenu_Prev(ent);
		return;
	} else if (cl->chase_target) {
		ChasePrev(ent);
		return;
	}
//ZOID

Congrats, you now have the PMenu system. Now, let's implement a "Join Game" menu together! All follow me now!

[page=Example Menu: Step 1]

The first step is to make our actual menu framework. I recommend adding this guide at the bottom of p_menu.c for reference:

/*
==================
Paril's Menus

Generic Menu
  Thanks to ZOID for Pmenu
==================
pmenu_t genericmenu[] = {
	{ NULL,					PMENU_ALIGN_CENTER, NULL },
	{ NULL,                	PMENU_ALIGN_CENTER, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_RIGHT, NULL },
};

void GenericMenuOpen (edict_t *ent, pmenuhnd_t *p)
{
	PMenu_Close(ent);
	PMenu_Open(ent, genericmenu, -1, sizeof(genericmenu) / sizeof(pmenu_t), NULL);
}*/

It shouldn't be too hard to learn.
Now, let's make a Join Game menu together!!

Alright, I won't post the functions to enter you in the game, you should be able to make it without me (Or check my Class tutorial that will be here soon!)

[page=Example Menu: Step 2]

First, we set up our struct. I named it "joinmenu", you can name it whatever you want.
Please note I am doing this WITHOUT C++ with me, so please notify me of errors.

#include "g_local.h"
#include "p_menu.h"

void JoinUp (edict_t *ent, pmenuhnd_t *p);

pmenu_t joinmenu[] = {
	{ "*Quake II",					PMENU_ALIGN_CENTER, NULL },
	{ "*Welcome To My Mod!",                	PMENU_ALIGN_CENTER, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ "Join Game",					PMENU_ALIGN_LEFT, JoinUp },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{ NULL,					PMENU_ALIGN_LEFT, NULL },
	{"My mod version 7",					PMENU_ALIGN_RIGHT, NULL },
};

void JoinUp (edict_t *ent, pmenuhnd_t *p)
{
    gi.dprintf ("%s joined the game!", ent->client->pers.netname);
    EndObserverMode (ent);
}

That's it. Now you have a small working menu. You can even link menus to menus. Now, next part.

[page=Example Menu: Step 3]
If we want our menu to work, we need to link it to something, don't we?
Alright, head to g_cmds.c, and up at the top below the includes let's include p_menu.h, and add:

void JoinMenuOpen (edict_t *ent)
{
	PMenu_Close(ent);
	PMenu_Open(ent, joinmenu, -1, sizeof(joinmenu) / sizeof(pmenu_t), NULL);
}*/

Now, link that to maybe cmd_inven, adding like this:

if (ent->client->resp.team <= 0)
    JoinMenuOpen (ent);

or make a new cmd to link it. You know how to do that.. don't you?

[page=The End]
Well, that's all for me, and the end of the tutorial. Hopefully the PMenu system is better than any others, and you can use the PMenu for alot of things, even ASCII.

Hopefully the last two parts worked, my D drive is gone for a bit, no Q2 or C++. That's all I know on this. Expect many more tutorials from me!

-Paril Kalashnikov

Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: