Dune 2: The Building of a Dynasty

    Developer: Westwood Studios
    Publisher: Virgin Games
    Genre: Strategy (Real-time) / Top-down
    System Requirements:
         moder browsers


    Instead of introducing


    Dune 2 is a great strategy, one of the first in the genre. There is no point in kicking and saying how great this game is for an entire generation of 90s children. And since I unexpectedly find it fun to mess with the code and port it to JavaScript, of course, after TTD ( article ) , of course, Dune 2 inevitably became my goal . Fortunately, I didn’t think of starting with it, because I’m afraid I wouldn’t coped. As it turned out, even though Dune 2 is simpler in functionality than TTD, it was more difficult to port it, but more on that later.

    Code base


    The choice of the “correct” code base is the main factor in the successful porting of a project using emscripten . For example, the use of SDL , the lack of multithreading are a good marker that porting will be a success. I went through all the projects in one way or another related to Dune 2, and settled on OpenDune . The thing that caught me was a complete copy of all the behavior of the original game, including all its bugs. It looks like the code for this project was originally obtained semi-automatically from the original. In the code here and there are variables called local_03FF, a lot of global variables, the code is very hard to read. The most serious drawback of the source code base is multithreading; it caused a lot of problems when porting. But the result is really good, in the browser the game is very similar to the original, except for a new pack of bugs.

    So, the dry facts:

    Language: C
    Number of source files: 143
    Number of lines of code: 59151
    Size of the binary: 423.2 Kb
    Size of equivalent JavaScript: ~ 1000 Kb
    Time spent on porting: ~ 2 months

    Later in this article we will describe the difficulties that I encountered while porting. Surely this is not interesting for everyone, if so, then lower this subsection to “known problems”.

    Multithreading VS Asynchrony


    OpenDune has an interesting interrupt-based multithreading model. To ensure multi-threading, the game code at the time of idle spins in endless cycles, it looks something like this:

    	while (true) {
    		uint16 key;
    		key = GUI_Widget_HandleEvents(w);
    		if (key = 13) {
    			break;
    		}
    		sleepIdle();
    	}
    


    When the application starts, the interval timer is initialized with the setitimer function . This timer causes interruption at regular intervals. It pauses the main thread of execution and allows arbitrary code to be executed. For JavaScript, the implementation of a similar timer is trivial, however, a different porting method was chosen so as not to artificially divide the project into JavaScript and C implementations. It was decided to completely abandon the use of the setitimer function , instead, the call to sleepIdle () was replaced by a timer event handling function, i.e. instead of downtime, this function determines which scheduled events have arrived and launches them for execution.

    More serious problem - inside while loops, any appearance of such a loop in JavaScript will cause an inevitable freeze of the open tab of the browser (or the browser as a whole). This is due to the fact that most loops expect user input (clicking the mouse button, keyboard), however, the browser cannot process events from input devices, they are put into the execution chain after the current executable JavaScript block. A possible way to solve this problem is to manually edit the code and transfer the problem code to asynchronous mode.

    A small example. Here is a draft code that causes problems:

    void someProblemFunction() {
    	{
    		//open 1
    	}
    	while (true) {
    		// open 2
    		while (true) {
    			// code 2
    		}
    		// close 2
    	}
    	{
    		//close 2
    	}
    }
    


    After excruciating speculative manipulations, the asynchronous code:

    void asyncSomeProblemFunction() {
    	Async_InvokeInLoop(
    		asyncSomeProblemFunctionOpen1,
    		asyncSomeProblemFunctionCondition1,
    		asyncSomeProblemFunctionLoop1,
    		asyncSomeProblemFunctionClose1);
    }
    void asyncSomeProblemFunctionOpen1() {
    	// code from open 1
    }
    void asyncSomeProblemFunctionCondition1() {
    	// code from loop 1 condition
    }
    void asyncSomeProblemFunctionLoop1() {
    	Async_InvokeInLoop(
    		asyncSomeProblemFunctionOpen2,
    		asyncSomeProblemFunctionCondition2,
    		asyncSomeProblemFunctionLoop2,
    		asyncSomeProblemFunctionClose2);
    }
    void asyncSomeProblemFunctionClose1() {
    	// code from close 1
    }
    


    Infernal work. The core of the entire system is the Async_InvokeInLoop function .

    void Async_InvokeInLoop(
    	void (*open)(), 
    	void (*condition)(bool* ref), 
    	void (*loop)(), 
    	void (*close)());
    


    Async_InvokeInLoop - allows you to replace any while (true) loop with an asynchronous equivalent. The function guarantees the call open before the start of the loop, and close after the end of the loop. References to the condition and loop functions are equal participants in the asynchronous iteration, which they make clear from the name. Iteration is implemented through the Async_Loop function :

    void Async_Loop() {
    	ScheduledAsync *top = STACK_TOP;
    	switch (top->state) {
    		case ScheduledAsync_OPEN: {
    			top->open();
    			top->state = ScheduledAsync_CONDITION;
    			return;
    		}
    		case ScheduledAsync_CONDITION: {
    			top->condition(&top->conditionValue);
    			top->state = ScheduledAsync_LOOP;
    			return;
    		}
    		case ScheduledAsync_LOOP: {
    			if (top->conditionValue) {
    				top->loop();
    				top->state = ScheduledAsync_CONDITION;
    			} else {
    				top->state = ScheduledAsync_CLOSE;
    			}
    			return;
    		}
    		case ScheduledAsync_CLOSE: {
    			popStack();
    			top->close();
    			free(top);
    			return;
    		}
    		default:
    			abort();
    	}
    }
    


    The game loop (or timer in JavaScript) periodically yanks this function, forcing everything in the game to spin. If the original function should return the result, then the problems double - you have to save the result in memory globally, and then retrieve it in other functions. Everything works by agreement. As a result, I got a hell of a framework for asynchronizing a project, here is its interface:

    /*
     * async.h
     *
     *  Created on: 19.10.2012
     *      Author: caiiiycuk
     */
    #ifndef ASYNC_H_
    #define ASYNC_H_
    #include "types.h"
    extern void async_noop();
    extern void async_false(bool *condition);
    extern void async_true(bool *condition);
    extern void	Async_InvokeInLoop(void (*open)(), void (*condition)(bool* ref), void (*loop)(), void (*close)());
    extern bool	Async_IsPending();
    extern void	Async_Loop();
    extern void	Async_InvokeAfterAsync(void (*callback)());
    extern void	Async_InvokeAfterAsyncOrNow(void (*callback)());
    extern void	Async_Storage_uint16(uint16* storage);
    extern void	Async_StorageSet_uint16(uint16 value);
    #endif /* ASYNC_H_ */
    


    The synchronous nature of the game mutated into asynchronous, which struck several funny bugs:
    • If you call the construction menu immediately before the computer opponent determines the next building to be built, then you can access its structures (fixed)
    • When loading the script, it was possible that enemy structures would receive 20,000 - 30,000 units of life instead of 150-200 (fixed)
    • Due to synchronization errors - the game map can be redrawn directly on top of the dialogue with the mentate, though this rarely appears (not fixed)


    Known Issues


    Due to the fact that the staff of testers consists of me and my fictional friends, we just know:
    • The game works in browsers Firefox, Chrome, Opera, Chrome (Android ~ 4)
    • The game was completely completed for the Harkonnen house and no serious problems were found
    • A small number of missions completed in two other houses, there were also no problems
    • The game cursor never changes (regardless of the selected action), intentionally done (it slows down)
    • To scroll the map, use the minimap or the arrows of the keyboard (use the keyboard in the game more)
    • There is music, but no effects
    • Artifacts may appear on the game map (very rarely, you will immediately understand), in this case opening / closing the game menu helps
    • The menu has items: save, load, restart mission
    • There is only one save slot in the game (for all houses)

    Everything else works, or should work.
    Tracker: https://github.com/caiiiycuk/play-dune

    Are we playing


    http://play-dune.com/

    71

    UPD1. Hot keys: habrahabr.ru/post/159501/#comment_5516325

    UPD2. The server asks, I give direct links with statics:
    Play for the house Atreides
    Play for the house Ordos
    Play for the house Harkonnen

    UPD3. The server ran into tcpsndbuf restriction - the total size of buffers that can be used to send data via TCP connections. The limitation that the provider sets me, I can not afford a more efficient server, sorry if you were not able to play. We are waiting for the load to return to normal.

    UPD4. Nevertheless, the problem was in my crooked hands, now the server should cope with the load.

    UPD5.The Invoice button does not work in the spaceport, be careful if you have not ordered any units in the spaceport and clicked “Send Order”, then the “Invoice” button will automatically be pressed and everything will freeze.

    UPD6. New project site.

    Also popular now: