From b105684fbd435c584482bd586521c32c959a15ec Mon Sep 17 00:00:00 2001 From: mcmartin Date: Mon, 13 Nov 2006 11:49:41 +0000 Subject: [PATCH] New netplay UI. git-svn-id: svn://svn.code.sf.net/p/sc2/code/trunk@2500 8092fc87-c524-0410-9efc-e669fe64eaf9 --- sc2/ChangeLog | 1 + sc2/doc/users/netplay-howto.txt | 63 +-- sc2/src/sc2code/cnctdlg.c | 556 +++++++++++++++++++++++- sc2/src/sc2code/libs/graphics/widgets.h | 5 +- 4 files changed, 579 insertions(+), 46 deletions(-) diff --git a/sc2/ChangeLog b/sc2/ChangeLog index d88a3c13b..1217d1f9e 100644 --- a/sc2/ChangeLog +++ b/sc2/ChangeLog @@ -1,4 +1,5 @@ Changes towards version 0.6: +- Netplay configuration dialog - Michael - Better error message for 'Kernel failed to load' (#917) - Michael - Pushbutton for connecting to netplay, in anticipation of a full configuration dialog later - Michael diff --git a/sc2/doc/users/netplay-howto.txt b/sc2/doc/users/netplay-howto.txt index cd1a8ef0a..32310735c 100644 --- a/sc2/doc/users/netplay-howto.txt +++ b/sc2/doc/users/netplay-howto.txt @@ -1,42 +1,23 @@ -To use Netplay you need to pass some parameters to uqm. In future versions -you should be able to set everything you need to set from within the -game itself. - ---netport1 specifies the port the bottom player will connect to or accept - incoming connections on. If this parameter is not specified, 21837 will - be used. - Your firewall needs to be set up to allow TCP connections from/to - the used port. ---nethost1 specifies the name or ip number of the host to connect to for - the bottom player. If this parameter is not specified, UQM will - instead accept incoming connections. ---netport2 - As --netport1, but for the top player ---nethost2 - As --nethost1, but for the top player ---netdelay - The desired input delay (in frames). A keypress or keyrelease - will only take effect after this many frames. The result is that while - higher values make your ship to seem to respond more sluggishly, - they give the keypress information more time to reach the remote party. - If the game stutters, this is because it is waiting for this - keypress information to arrive, which is an indication that the - input delay is too low. - As SuperMelee runs at 24 frames per second, each frame delayed will - delay the input by about 42ms. - The delay used is the maximum of the desired value for both parties. - The default is 0. Values lower than 4 will be hardly noticable - in terms of responsiveness. - Future versions may automatically decide on the best value to use. - -In SuperMelee, there is now an extra button, "NET". When you push it, -UQM will start paying attention to incoming connections or try to -establish an outgoing connection, depending on the specified -parameters. Pressing SPACE at the connection screen will cancel -connection attempts. Once a connection has been established, the -input will be listed as "NETWORK CONTROL". To abort an already -established connection, change the control scheme away from NETWORK -CONTROL. The game won't start until both parties have confirmed the -current setup. If one player moves away from the 'Battle!' button, -both parties need to reconfirm. +In SuperMelee, there is now an extra button for each side, marked +"NET". When you push it, you will get the net configuration dialog. +To have someone else connect to you, choose the first option. +Otherwise, edit the host name and choose "connect". Pressing SPACE on +the resulting connection screen will cancel connection attempts. Once +a connection has been established, the input will be listed as +"NETWORK CONTROL". To abort an already established connection, change +the control scheme away from NETWORK CONTROL. The game won't start +until both parties have confirmed the current setup. If one player +moves away from the 'Battle!' button, both parties need to reconfirm. +In addition to setting the hostname, you can also configure the port +and the input delay in frames. A keypress or keyrelease will only take +effect after this many frames. The result is that while higher values +make your ship to seem to respond more sluggishly, they give the +keypress information more time to reach the remote party. If the game +stutters, this is because it is waiting for this keypress information +to arrive, which is an indication that the input delay is too low. As +SuperMelee runs at 24 frames per second, each frame delayed will delay +the input by about 42ms. The delay used is the maximum of the desired +value for both parties. The default is 0. Values lower than 4 will be +hardly noticable in terms of responsiveness. Future versions may +automatically decide on the best value to use. diff --git a/sc2/src/sc2code/cnctdlg.c b/sc2/src/sc2code/cnctdlg.c index 736d67bed..be0aa7b2f 100644 --- a/sc2/src/sc2code/cnctdlg.c +++ b/sc2/src/sc2code/cnctdlg.c @@ -20,32 +20,575 @@ #include "cnctdlg.h" #include "controls.h" +#include "colors.h" +#include "setup.h" +#include "libs/graphics/widgets.h" +#include "netplay/netoptions.h" + +#define MCD_WIDTH 260 +#define MCD_HEIGHT 100 + +#define MENU_FRAME_RATE (ONE_SECOND / 20) typedef struct connect_dialog_state { BOOLEAN (*InputFunc) (struct connect_dialog_state *pInputState); COUNT MenuRepeatDelay; + DWORD NextTime; BOOLEAN Initialized; int which_side; - int option; + int confirmed; } CONNECT_DIALOG_STATE; +static void DrawConnectDialog (void); + typedef CONNECT_DIALOG_STATE *PCONNECT_DIALOG_STATE; +static WIDGET_MENU_SCREEN menu; +static WIDGET_BUTTON buttons[3]; +static WIDGET_SLIDER slider; +static WIDGET_TEXTENTRY texts[2]; + +static WIDGET *menu_widgets[] = { + (WIDGET *)&buttons[1], + (WIDGET *)&texts[0], + (WIDGET *)&buttons[0], + (WIDGET *)&slider, + (WIDGET *)&texts[1], + (WIDGET *)&buttons[2] }; + +static BOOLEAN done; + +/* This kind of sucks, but the Button callbacks need access to the + * CONNECT_DIALOG_STATE, so we need a pointer to it */ + +static PCONNECT_DIALOG_STATE current_state; + +static int do_connect (WIDGET *self, int event); +static int do_listen (WIDGET *self, int event); +static int do_cancel (WIDGET *self, int event); + +static void +MCD_DrawMenuScreen (WIDGET *_self, int x, int y) +{ + int widget_index, widget_y; + + WIDGET_MENU_SCREEN *self = (WIDGET_MENU_SCREEN *)_self; + + widget_y = y + 8; + for (widget_index = 0; widget_index < self->num_children; widget_index++) + { + WIDGET *c = self->child[widget_index]; + (*c->draw)(c, x, widget_y); + widget_y += (*c->height)(c) + 8; + } +} + +static void +MCD_DrawButton (WIDGET *_self, int x, int y) +{ + WIDGET_BUTTON *self = (WIDGET_BUTTON *)_self; + COLOR oldtext; + COLOR inactive, selected; + FONT oldfont = SetContextFont (StarConFont); + FRAME oldFontEffect = SetContextFontEffect (NULL); + TEXT t; + + selected = MENU_HIGHLIGHT_COLOR; + inactive = MENU_TEXT_COLOR; + + t.baseline.x = 160; + t.baseline.y = y; + t.align = ALIGN_CENTER; + t.CharCount = ~0; + t.pStr = self->name; + if (widget_focus == _self) + { + oldtext = SetContextForeGroundColor (selected); + } + else + { + oldtext = SetContextForeGroundColor (inactive); + } + font_DrawText (&t); + SetContextFontEffect (oldFontEffect); + SetContextFont (oldfont); + SetContextForeGroundColor (oldtext); + (void) x; +} + +static void +MCD_DrawSlider (WIDGET *_self, int x, int y) +{ + WIDGET_SLIDER *self = (WIDGET_SLIDER *)_self; + COLOR oldtext; + COLOR default_color, selected; + FONT oldfont = SetContextFont (StarConFont); + FRAME oldFontEffect = SetContextFontEffect (NULL); + TEXT t; + RECT r; + int tick = (MCD_WIDTH) / 8; + + default_color = MENU_TEXT_COLOR; + selected = MENU_HIGHLIGHT_COLOR; + + t.baseline.x = x; + t.baseline.y = y; + t.align = ALIGN_LEFT; + t.CharCount = ~0; + t.pStr = self->category; + if (widget_focus == _self) + { + oldtext = SetContextForeGroundColor (selected); + } + else + { + oldtext = SetContextForeGroundColor (default_color); + } + font_DrawText (&t); + + r.corner.x = t.baseline.x + 3 * tick; + r.corner.y = t.baseline.y - 4; + r.extent.height = 2; + r.extent.width = 3 * tick; + DrawFilledRectangle (&r); + + r.extent.width = 3; + r.extent.height = 8; + r.corner.y = t.baseline.y - 7; + r.corner.x = t.baseline.x + 3 * tick + (3 * tick * (self->value - self->min) / + (self->max - self->min)) - 1; + DrawFilledRectangle (&r); + + (*self->draw_value)(self, t.baseline.x + 7 * tick, t.baseline.y); + + SetContextFontEffect (oldFontEffect); + SetContextFont (oldfont); + SetContextForeGroundColor (oldtext); +} + +void +MCD_DrawTextEntry (WIDGET *_self, int x, int y) +{ + WIDGET_TEXTENTRY *self = (WIDGET_TEXTENTRY *)_self; + COLOR oldtext; + COLOR inactive, default_color, selected; + FONT oldfont = SetContextFont (StarConFont); + FRAME oldFontEffect = SetContextFontEffect (NULL); + TEXT t; + + default_color = MENU_TEXT_COLOR; + selected = MENU_HIGHLIGHT_COLOR; + inactive = MENU_TEXT_COLOR; + + BatchGraphics (); + + t.baseline.x = x; + t.baseline.y = y; + t.align = ALIGN_LEFT; + t.CharCount = ~0; + t.pStr = self->category; + if (widget_focus == _self) + { + oldtext = SetContextForeGroundColor (selected); + } + else + { + oldtext = SetContextForeGroundColor (default_color); + } + font_DrawText (&t); + + /* Force string termination */ + self->value[WIDGET_TEXTENTRY_WIDTH-1] = 0; + + t.baseline.y = y; + t.CharCount = utf8StringCount (self->value); + t.pStr = self->value; + + if (!(self->state & WTE_EDITING)) + { // normal or selected state + t.baseline.x = 160; + t.align = ALIGN_CENTER; + + if (widget_focus == _self) + { + oldtext = SetContextForeGroundColor (selected); + } + else + { + oldtext = SetContextForeGroundColor (inactive); + } + font_DrawText (&t); + } + else + { // editing state + COUNT i; + RECT text_r; + BYTE char_deltas[WIDGET_TEXTENTRY_WIDTH]; + PBYTE pchar_deltas; + RECT r; + SIZE leading; + + t.baseline.x = x + 90; + t.align = ALIGN_LEFT; + + // calc background box dimensions + // XXX: this may need some tuning, especially if a + // different font is used. The font 'leading' values + // are not what they should be. +#define BOX_VERT_OFFSET 2 + GetContextFontLeading (&leading); + r.corner.x = t.baseline.x - 1; + r.corner.y = t.baseline.y - leading + BOX_VERT_OFFSET; + r.extent.width = MCD_WIDTH - r.corner.x - 10; + r.extent.height = leading + 2; + + TextRect (&t, &text_r, char_deltas); +#if 0 + // XXX: this should potentially be used in ChangeCallback + if ((text_r.extent.width + 2) >= r.extent.width) + { // the text does not fit the input box size and so + // will not fit when displayed later + UnbatchGraphics (); + // disallow the change + return (FALSE); + } +#endif + + oldtext = SetContextForeGroundColor (selected); + DrawFilledRectangle (&r); + + // calculate the cursor position and draw it + pchar_deltas = char_deltas; + for (i = self->cursor_pos; i > 0; --i) + r.corner.x += (SIZE)*pchar_deltas++; + if (self->cursor_pos < t.CharCount) /* cursor mid-line */ + --r.corner.x; + if (self->state & WTE_BLOCKCUR) + { // Use block cursor for keyboardless systems + if (self->cursor_pos == t.CharCount) + { // cursor at end-line -- use insertion point + r.extent.width = 1; + } + else if (self->cursor_pos + 1 == t.CharCount) + { // extra pixel for last char margin + r.extent.width = (SIZE)*pchar_deltas + 2; + } + else + { // normal mid-line char + r.extent.width = (SIZE)*pchar_deltas + 1; + } + } + else + { // Insertion point cursor + r.extent.width = 1; + } + // position cursor within input field rect + ++r.corner.x; + ++r.corner.y; + r.extent.height -= 2; + SetContextForeGroundColor (MENU_TEXT_COLOR); + DrawFilledRectangle (&r); + + SetContextForeGroundColor (inactive); + font_DrawText (&t); + } + + UnbatchGraphics (); + SetContextFontEffect (oldFontEffect); + SetContextFont (oldfont); + SetContextForeGroundColor (oldtext); +} + +/* Text entry stuff, mostly C&Ped from setupmenu.c. Could use some + * refactoring, as redraw_menu () is the only real change. */ + +static BOOLEAN +OnTextEntryChange (PTEXTENTRY_STATE pTES) +{ + WIDGET_TEXTENTRY *widget = (WIDGET_TEXTENTRY *) pTES->CbParam; + + widget->cursor_pos = pTES->CursorPos; + if (pTES->JoystickMode) + widget->state |= WTE_BLOCKCUR; + else + widget->state &= ~WTE_BLOCKCUR; + + // XXX TODO: Here, we can examine the text entered so far + // to make sure it fits on the screen, for example, + // and return FALSE to disallow the last change + + return TRUE; // allow change +} + +static BOOLEAN +OnTextEntryFrame (PTEXTENTRY_STATE pTES) +{ + WIDGET_TEXTENTRY *widget = (WIDGET_TEXTENTRY *) pTES->CbParam; + + DrawConnectDialog (); + + SleepThreadUntil (pTES->NextTime); + pTES->NextTime = GetTimeCounter () + MENU_FRAME_RATE; + + return TRUE; // continue +} + +static int +OnTextEntryEvent (WIDGET_TEXTENTRY *widget) +{ // Going to edit the text + TEXTENTRY_STATE tes; + UNICODE revert_buf[256]; + + // position cursor at the end of text + widget->cursor_pos = utf8StringCount (widget->value); + widget->state = WTE_EDITING; + DrawConnectDialog (); + + // make a backup copy for revert on cancel + utf8StringCopy (revert_buf, sizeof (revert_buf), widget->value); + + // text entry setup + tes.Initialized = FALSE; + tes.MenuRepeatDelay = 0; + tes.NextTime = GetTimeCounter () + MENU_FRAME_RATE; + tes.BaseStr = widget->value; + tes.MaxSize = widget->maxlen; + tes.CursorPos = widget->cursor_pos; + tes.CbParam = widget; + tes.ChangeCallback = OnTextEntryChange; + tes.FrameCallback = OnTextEntryFrame; + + // SetMenuSounds (0, MENU_SOUND_SELECT); + if (!DoTextEntry (&tes)) + { // editing failed (canceled) -- revert the changes + utf8StringCopy (widget->value, widget->maxlen, revert_buf); + } + else + { + if (widget->onChange) + { + (*(widget->onChange))(widget); + } + } + + widget->state = WTE_NORMAL; + DrawConnectDialog (); + + return TRUE; // event handled +} + +/* Button response routines */ + +static int +do_connect (WIDGET *self, int event) +{ + if (event == WIDGET_EVENT_SELECT) + { + /* These assignments are safe exactly because texts[] is file-scope) */ + netplayOptions.peer[current_state->which_side].host = texts[0].value; + netplayOptions.peer[current_state->which_side].port = texts[1].value; + netplayOptions.peer[current_state->which_side].isServer = FALSE; + current_state->confirmed = TRUE; + netplayOptions.inputDelay = slider.value; + + done = TRUE; + } + (void)self; + return FALSE; +} + +static int +do_listen (WIDGET *self, int event) +{ + if (event == WIDGET_EVENT_SELECT) + { + /* These assignments are safe exactly because texts[] is file-scope) */ + netplayOptions.peer[current_state->which_side].port = texts[1].value; + netplayOptions.peer[current_state->which_side].isServer = TRUE; + netplayOptions.inputDelay = slider.value; + current_state->confirmed = TRUE; + done = TRUE; + } + (void)self; + return FALSE; +} + +static int +do_cancel (WIDGET *self, int event) +{ + if (event == WIDGET_EVENT_SELECT) + { + current_state->confirmed = FALSE; + done = TRUE; + } + (void)self; + return FALSE; +} + + +static void +CreateWidgets (void) +{ + int i; + + done = false; + + for (i = 0; i < 3; i++) + { + buttons[i].tag = WIDGET_TYPE_BUTTON; + buttons[i].parent = NULL; + buttons[i].receiveFocus = Widget_ReceiveFocusSimple; + buttons[i].draw = MCD_DrawButton; + buttons[i].height = Widget_HeightOneLine; + buttons[i].width = Widget_WidthFullScreen; + } + buttons[0].name = "Connect to remote host"; + buttons[1].name = "Wait for incoming connection"; + buttons[2].name = "Cancel"; + + buttons[0].handleEvent = do_connect; + buttons[1].handleEvent = do_listen; + buttons[2].handleEvent = do_cancel; + + menu.tag = WIDGET_TYPE_MENU_SCREEN; + menu.parent = NULL; + menu.receiveFocus = Widget_ReceiveFocusMenuScreen; + menu.draw = MCD_DrawMenuScreen; + menu.height = Widget_HeightFullScreen; + menu.width = Widget_WidthFullScreen; + menu.num_children = 6; + menu.child = menu_widgets; + menu.handleEvent = Widget_HandleEventMenuScreen; + + slider.tag = WIDGET_TYPE_SLIDER; + slider.parent = NULL; + slider.handleEvent = Widget_HandleEventSlider; + slider.receiveFocus = Widget_ReceiveFocusSimple; + slider.draw = MCD_DrawSlider; + slider.height = Widget_HeightOneLine; + slider.width = Widget_WidthFullScreen; + slider.draw_value = Widget_Slider_DrawValue; + slider.min = 0; + slider.max = 9; + slider.step = 1; + slider.value = netplayOptions.inputDelay; + slider.category = "Net Delay"; + + for (i = 0; i < 2; i++) + { + texts[i].tag = WIDGET_TYPE_TEXTENTRY; + texts[i].parent = NULL; + texts[i].handleEvent = Widget_HandleEventTextEntry; + texts[i].receiveFocus = Widget_ReceiveFocusSimple; + texts[i].draw = MCD_DrawTextEntry; + texts[i].height = Widget_HeightOneLine; + texts[i].width = Widget_WidthFullScreen; + texts[i].handleEventSelect = OnTextEntryEvent; + texts[i].maxlen = WIDGET_TEXTENTRY_WIDTH-1; + texts[i].state = WTE_NORMAL; + texts[i].cursor_pos = 0; + } + + texts[0].category = "Host"; + texts[1].category = "Port"; + + /* We sometimes assign to these internals; cannot strncpy over self! */ + if (texts[0].value != netplayOptions.peer[current_state->which_side].host) + { + strncpy (texts[0].value, netplayOptions.peer[current_state->which_side].host, texts[0].maxlen); + } + if (texts[1].value != netplayOptions.peer[current_state->which_side].port) + { + strncpy (texts[1].value, netplayOptions.peer[current_state->which_side].port, texts[1].maxlen); + } + texts[0].value[texts[0].maxlen]=0; + texts[1].value[texts[1].maxlen]=0; + + menu.receiveFocus ((WIDGET *)&menu, WIDGET_EVENT_DOWN); +} + +static void +DrawConnectDialog (void) +{ + RECT r; + + r.extent.width = MCD_WIDTH; + r.extent.height = MCD_HEIGHT; + r.corner.x = (SCREEN_WIDTH - r.extent.width) >> 1; + r.corner.y = (SCREEN_HEIGHT - r.extent.height) >> 1; + + LockMutex (GraphicsLock); + + DrawShadowedBox (&r, SHADOWBOX_BACKGROUND_COLOR, + SHADOWBOX_DARK_COLOR, SHADOWBOX_MEDIUM_COLOR); + + menu.draw ((WIDGET *)&menu, r.corner.x + 10, r.corner.y + 10); + + UnlockMutex (GraphicsLock); +} + static BOOLEAN DoMeleeConnectDialog (PCONNECT_DIALOG_STATE state) { + BOOLEAN changed; + if (!state->Initialized) { state->Initialized = TRUE; SetDefaultMenuRepeatDelay (); + state->NextTime = GetTimeCounter (); /* Prepare widgets, draw stuff, etc. */ + CreateWidgets (); + DrawConnectDialog (); } - /* But this is skeletal, so just end immediately */ - return FALSE; + changed = TRUE; + + if (PulsedInputState.menu[KEY_MENU_UP]) + { + Widget_Event (WIDGET_EVENT_UP); + } + else if (PulsedInputState.menu[KEY_MENU_DOWN]) + { + Widget_Event (WIDGET_EVENT_DOWN); + } + else if (PulsedInputState.menu[KEY_MENU_LEFT]) + { + Widget_Event (WIDGET_EVENT_LEFT); + } + else if (PulsedInputState.menu[KEY_MENU_RIGHT]) + { + Widget_Event (WIDGET_EVENT_RIGHT); + } + else if (PulsedInputState.menu[KEY_MENU_SELECT]) + { + Widget_Event (WIDGET_EVENT_SELECT); + } + else if (PulsedInputState.menu[KEY_MENU_CANCEL]) + { + Widget_Event (WIDGET_EVENT_CANCEL); + } + else if (PulsedInputState.menu[KEY_MENU_DELETE]) + { + Widget_Event (WIDGET_EVENT_DELETE); + } + else + { + changed = FALSE; + } + + if (changed) + { + DrawConnectDialog (); + } + + SleepThreadUntil (state->NextTime + MENU_FRAME_RATE); + state->NextTime = GetTimeCounter (); + return !((GLOBAL (CurrentActivity) & CHECK_ABORT) || + done); } BOOLEAN @@ -56,10 +599,15 @@ MeleeConnectDialog (int side) state.Initialized = FALSE; state.which_side = side; state.InputFunc = DoMeleeConnectDialog; + state.confirmed = TRUE; + + current_state = &state; DoInput ((PVOID)&state, TRUE); - return TRUE; + current_state = NULL; + + return state.confirmed; } #endif /* NETPLAY */ diff --git a/sc2/src/sc2code/libs/graphics/widgets.h b/sc2/src/sc2code/libs/graphics/widgets.h index 854086465..30c11086b 100644 --- a/sc2/src/sc2code/libs/graphics/widgets.h +++ b/sc2/src/sc2code/libs/graphics/widgets.h @@ -43,7 +43,7 @@ typedef enum { NUM_WIDGET_TYPES } WIDGET_TYPE; -#define WIDGET_TEXTENTRY_WIDTH 30 +#define WIDGET_TEXTENTRY_WIDTH 50 #define WIDGET_CONTROLENTRY_WIDTH 16 typedef struct _widget { @@ -213,4 +213,7 @@ void Widget_DrawControlEntry (WIDGET *_self, int x, int y); void Widget_Slider_DrawValue (WIDGET_SLIDER *self, int x, int y); +/* Other implementations will need these values */ +extern WIDGET *widget_focus; + #endif /* _WIDGETS_H */