jjSTREAM (file reading/writing and network packets)

The jjSTREAM class is used to save/transfer specific data. Its main functions are push and pop. You can create a jjSTREAM variable, and push any values onto it. Then, when you need to read from it, you pop them. jjSTREAM is a container working on FIFO basis, i.e. the variables are popped in the same order they were pushed. For example:

jjSTREAM myStream; //create an empty stream
int a = 4;
string b = "TexT";
bool c = false;
myStream.push(a); //push the int "a" onto the stream
myStream.push(b); //push the string "b" onto the stream
myStream.push(c); //push the bool "c" onto the stream
int d;
string e;
bool f;
myStream.pop(d); //the int "d" will have the same value as pushed "a"
myStream.pop(e); //the string "e" will have the same value as pushed "b"
myStream.pop(f); //the bool "f" will have the same value as pushed "c"
//now the stream is empty again.

It's very important to keep the order of removing variables from jjSTREAM the same as the order of inserting them into it, as well as keep consistency in use of variable types. Attempts to pop a variable of different type than the pushed type will not be blocked but will generally result in obtaining a wrong section of data.

As you can imagine, the potential for usage of this class as a container is fairly limited and most of the time you'd prefer to use the built-in array class instead. The real strengths of jjSTREAM show when you implement networking or file manipulation in your script.

To demonstrate file manipulation capabilities of jjSTREAM, let's assume your single player campaign requires that various items be carried between levels. JJ2+ doesn't currently offer means of transfering custom data between separate levels, but it allows you to save a temporary file in one level that you can then load in another. The following code shows possible implementation of functions that would do so.

array<string> items; //In this example we assume items are stored as text strings, representing their names. However, any other basic type, such as int identifiers, would work as well.
bool saveItems() {
    jjSTREAM file;
    file.push(items.length()); //The first uint in the file will represent the number of items.
    for (uint i = 0; i < items.length(); i++) {
        file.push(items[i]); //The rest of the file will consist of elements of the items array.
    }
    return file.save("items.asdat"); //Return whether successful.
}
bool loadItems() {
    jjSTREAM file("items.asdat");
    uint length;
    if (file.pop(length)) { //Pop the first uint, assumed to be the number of items. If anything fails, abort the entire operation.
        items.resize(length);
        bool success = true;
        for (int i = 0; i < items.length() && success; i++) {
            success = file.pop(items[i]); //Retrieve consecutive elements of the items array from the file in the same order they were saved.
        }
        return success;
    }
    return false;
}

The exact way you write and read files will of course vary depending on what information you need to save, but you can expect it to be generally based around this scheme.

Networking works in a similar way as file manipulation but has its own quirks, especially due to the distinction between a server and clients. We'll demonstrate it using a fragment of a hypothetical script that allows players to press buttons and pull levers, each time informing all players in the server about the performed action. We'll think of a button as of something that doesn't have a state, and each time it's pressed it performs the same action. On the other hand, a lever will be something that at any given time can be either on or off. The fragment assumes that all buttons and levers in the level already have unique IDs assigned to them elsewhere, that are the same for the server and all clients.

enum packet_type {packet_button, packet_lever, packet_all_levers}; //We enumerate possible packet types. You will probably want to do this in almost every script that uses streams, as it simplifies the process of giving them more than one purpose. In our case, we want packets that inform about pressing buttons, pulling levers, and a special packet that informs newly joining clients about states of all levers in the level.
array<bool> leverStates;
void performButtonAction(jjPLAYER@ player, int buttonID) {
   //...
}
void sendButtonPacket(int8 playerID, int buttonID, int skippedClientID) {
   jjSTREAM packet;
   packet.push(uint8(packet_button));
   packet.push(playerID);
   packet.push(buttonID);
   jjSendPacket(packet, -skippedClientID);
}
void pressButton(int8 playerID, int buttonID) {
   performButtonAction(jjPlayers[playerID], buttonID);
   sendButtonPacket(playerID, buttonID, 0);
}
void sendLeverPacket(int8 playerID, int leverID, int skippedClientID) {
   jjSTREAM packet;
   packet.push(uint8(packet_lever));
   packet.push(playerID);
   packet.push(leverID);
   packet.push(leverStates[leverID]);
   jjSendPacket(packet, -skippedClientID);
}
void pullLever(int8 playerID, int leverID) {
   leverStates[leverID] = !leverStates[leverID];
   sendLeverPacket(playerID, leverID, 0);
}
void onLevelLoad() {
   if (!jjIsServer) {
      jjSTREAM packet;
      packet.push(uint8(packet_all_levers));
      jjSendPacket(packet); //When the level loads for clients, they request status of all levers from the server by sending a packet_all_levers packet.
   }
}
void onReceive(jjSTREAM &in packet, int clientID) {
   uint8 type;
   packet.pop(type);
   if (jjIsServer) {
      switch (type) {
         case packet_button: //A client reports having pressed a button.
            {
               int8 playerID;
               packet.pop(playerID);
               if (jjPlayers[playerID].clientID == clientID) { //Check if the received player ID really belongs to the client who sent the packet. Otherwise we might have to do with a hacking attempt.
                  int buttonID;
                  packet.pop(buttonID);
                  performButtonAction(jjPlayers[playerID], buttonID);
                  sendButtonPacket(playerID, buttonID, clientID);
               }
            }
            break;
         case packet_lever: //A client reports having pulled a lever.
            {
               int8 playerID;
               packet.pop(playerID);
               if (jjPlayers[playerID].clientID == clientID) { //Check if the received player ID really belongs to the client who sent the packet. Otherwise we might have to do with a hacking attempt.
                  int leverID;
                  packet.pop(leverID);
                  packet.pop(leverStates[leverID]);
                  sendLeverPacket(playerID, leverID, clientID);
               }
            }
            break;
         case packet_all_levers: //A client requests states of all levers.
            {
               jjSTREAM response;
               response.push(uint8(packet_all_levers));
               for (uint i = 0; i < leverStates.length(); i++) {
                  response.push(leverStates[i]);
               }
               jjSendPacket(response, clientID);
            }
            break;
         //A default case might also be in place to react to hacking attempts. To simplify the example it was not included.
      }
   } else {
      switch (type) {
         case packet_button: //The server informs about a button having been pressed.
            {
               int8 playerID;
               int buttonID;
               packet.pop(playerID);
               packet.pop(buttonID);
               performButtonAction(jjPlayers[playerID], buttonID);
            }
            break;
         case packet_lever: //The server informs about a lever having been pulled.
            {
               int8 playerID;
               int leverID;
               packet.pop(playerID);
               packet.pop(leverID);
               packet.pop(leverStates[leverID]);
            }
            break;
         case packet_all_levers: //The server responds to request for all lever states.
            for (uint i = 0; i < leverStates.length(); i++) {
               packet.pop(leverStates[i]);
            }
            break;
      }
   }
}

The example may seem arbitrary, but it contains all types of packets you want to send most of the time. Generally all network actions you will want to take are: report an event occurred (analogous to buttons), report a state of something changed (analogous to levers), or request state of the entire level when you join it (analogous to packet_all_levers in the above example).