Menu

Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Show posts Menu

Messages - DMWarrior

#1
Quote from: Athanatos on Feb 18, 2021, 05:34 PM
Quote from: Xmair on Feb 18, 2021, 08:54 AM
QuoteGiven the possibility of compiling one single EXE file, VC:MP could use it's own custom "multiplayer-only" build instead of relying on DLLs.
This point is something which caught my interest but I don't reckon VCMP developers will implement this for legal reasons.
R* devs don't give a crap about the 3D universe games anymore, to be honest. They are already milking enough money from GTA V / GTA Online

...and they gave a crap.

This reply was the best reason I could find on why this happened. Looks like the project is considered to be derivative work instead of clean room design.

On the other hand, this takedown was requested from Take-Two, the same company which tried to takedown OpenIV and only backpedalled after they got caught and received a lot of bad press. So who knows? Maybe it's just scare tactics.

I have to agree with Xmair and Sebastian. After what just happened, that doesn't seem to worth the risk, at least for now.
#2
Currently, there are some projects trying to achieve the goal of reconstructing GTA III and GTA Vice City through reverse engineering: re3 and reVC (miami branch). Development seems to be stable enough to the point people are using it to recompile and run these games on other platforms.

Could VC:MP take advantage of this in any way?

I can only imagine a few things of what could be achieved...
  • Playing VC:MP on consoles (Nintendo Switch, PlayStation Vita), or even on phones (if a source port is available in the future).
  • Since reVC includes some extra features and fixes (widescreen fix, PS2 effects), VC:MP players could just rely on what's already there instead of waiting to be included on an update.
  • Assuming that VC:MP code doesn't just relies on specific memory addresses for everything, having what's essentially the source code of the game could make things considerably easier or faster to implement. reVC could even be used as reference for feature requests or bug reports.
  • Given the possibility of compiling one single EXE file, VC:MP could use it's own custom "multiplayer-only" build instead of relying on DLLs.

Any thoughts on this?
#3
Community Plugins / Re: Lua Plugin
Nov 22, 2020, 10:58 AM
I made some minor changes on Blank Server:
Updated Blank Server (Lua script)

After setting up "package.path", we can easily load modules with require(). For anyone who's not familiar with Lua, here's an example:

MyModule.lua
----
-- @module MyModule
--
local M = {}

----
-- A function.
--
function M.hello()
  print("Hello, world!")
end

return M

Server script
local MyModule = require("MyModule")
MyModule.hello()

And since Lua has no classes, libraries like middleclass can be extremely useful, specially when combined with modules. It's easier to work with in comparison to Squirrel, at least in my opinion.

After noticing that VEH_ constants are missing, I decided to make a very simple list, too:
Vehicles List (Lua Script)

Nothing amazing or interesting yet, though, but it may gain some more interest once there's more scripts available to work with. One idea would be rewrite some scripts from Squirrel to Lua so they could be used as example or base for other scripts. I don't know if anyone would do it, but I could try doing this with some of my scripts to see how it looks.
#4
Community Plugins / Re: Lua Plugin
Nov 16, 2020, 09:00 PM
Good work! Lua support can be very interesting, not only due to it's popularity, but because it has more libraries avaliable than Squirrel, too.

I still don't know what I could do with it, though. But for anyone interested in giving a try, I managed to rewrite most of the VC:MP Blank Server to Lua, as well looking through each function and finding which ones are missing. The only problem is that SendDataToClient() doesn't work, but everything else seems to be fine.

This, alongside the Wiki, should be enough for getting started:
VC:MP Blank Server (Lua Script)
#5
Tutorials / Re: create_actor
May 01, 2020, 05:05 PM
Quote from: MEGAMIND on May 01, 2020, 03:46 PMwhile running node record.js no packets get sent or recived aftermintaing the process just a .json file is created with{"name":"","nameIndexAt":0,"nameAppearsAt":0,"packets":[]}so? do u install any npm module or what?

Here's a quick guide.

Setting up properties

The way I'm using to capture packets is by using a proxy. Basically, it works like this:



Now we're going to record on a local server. On the image above, we're using the default port used by VC:MP Blank Server, which is 8192, and port 3000 for our proxy.

On "record.js", find and set these property values:
// Server (proxy).
 "server": {
 "address"  : "localhost",
 "port"     : 3000, // <- RECORDER PORT
 "exclusive": true
 },

 // Server (VC:MP).
 "vcmp": {
 "address": "localhost",
 "port"   : 8192 // <- VC:MP SERVER PORT
 }

On "play.js", find and set these property values:
// Server (VC:MP).
 "vcmp": {
 "address": "localhost",
 "port"   : 8192 // <- VC:MP SERVER PORT
 }

Everything should be now set up! Open your VC:MP server and run the recorder with node record.js <playername>.

The <playername> refers to the name you have set on VC:MP.



If my name is set as "Player" on VC:MP, then I need to start the recorder using node record.js Player.

Entering the server

When our local server is added on favourites, we usually click on it and play. But in order to record our packets, we need to join on the proxy instead. Since we did set port 3000 for it, we need to add this server on our favourites now.



Now you'll have 2 servers on your list.



They're all the same, except the one we just set on port 3000 is saving all our packets and redirecting everything back to the real server. This is the one we need to join.

Join the server on port 3000 and play normally. Everything you do should is now being recorded by our proxy. There's no warning in game telling you that, everything is silently happening on our node.js server.



When you're done, close the node.js server. The file should be now full of contents inside. Also, if you see a message like << SERVER >> Found player name at packet <number>., then your name did match and everything is right!

Because I'm using "Player", the file generated will be "Player.json".

Spawning our bot

Now use node play.js <filename> <playername>. The playername refers to the name you want the bot to be called.

If I want to call it "Robot", then I need to use node play.js Player Robot.

Now we can just join the server normally (the one on port 8192). If everything is right, we should now see a player called "Robot" doing the same movements we did when we were on proxy!



When you're done with it, close the node.js server. The bot will eventually "timeout" and leave.

Quote from: habi on May 01, 2020, 05:02 PMfor a week, I was doing this. I can contribute to packet structure.
do you know how I decrypt packets? eg. I used to go to vector 0 0 0 in golf club, set gravity to 0, so player not falls. then study the packet

Unfortunately, no. I don't really understand how this works.
#6
Tutorials / Re: create_actor
May 01, 2020, 12:56 PM
Good job on this one, habi!

Far from perfect, of course, but it's way better than previous workarounds (which required you to join and open multiple games) and it can be used for testing or just to play around the city. And because you keep sending packets to keep them on server, they also could be used on things like shops or other locations.

Inspired by your idea, I decided to make something similar using node.js instead of Python. Here's my attempt.

Code used for recording packets (name it "record.js"):
[noae][noae]///////////////////////////////////////////////////////////////////////////////
// @file modules.js
///////////////////////////////////////////////////////////////////////////////
/** Arguments passed though command line. */
var args = process.argv.slice(2);

/** Filesystem module. */
var fs = require("fs");

/** Datagram module (used for UDP conections). */
var dgram = require("dgram");

/** Properties and settings. */
var properties = {
// Server (proxy).
"server": {
"address"  : "localhost",
"port"     : 1234, // <- RECORDER PORT
"exclusive": true
},

// Server (VC:MP).
"vcmp": {
"address": "localhost",
"port"   : 7777 // <- VC:MP SERVER PORT
},

// Client (proxy).
"client": {
"address": null,
"port"   : null
},

// Packet options.
"packets": {
"sent"                : 0,
"received"            : 0,
"displaySentEvery"    : 25,
"displayReceivedEvery": 25,
"foundName"           : false,
"name"                : "Default",
"recordingEnabled"    : true,
},

// Saved data.
"data": {
"name"         : "",
"nameIndexAt"  : 0,
"nameAppearsAt": 0,
"packets"      : []
}
};

// Change player name:
properties.packets.name = args[0] || properties.packets.name;

///////////////////////////////////////////////////////////////////////////////
// @file server.js
///////////////////////////////////////////////////////////////////////////////
/** UDP server. */
var server = dgram.createSocket("udp4");

/**
 * Event called by server when an error has occurred.
 *
 * @param {Error} error Error.
 */
server.on("error", function (error) {
  console.log("[SERVER] Error: " + error.stack);
  server.close();
});

/**
 * Event called by server when it received a message.
 *
 * @param {Buffer} message Message received.
 * @param {Object} remote Conection information.
 */
server.on("message", function (message, remote) {
// Save client's conection information. This will be necessary in order to
// redirect data to VC:MP later:
properties.client = {
"address": remote.address,
"port"   : remote.port
};

// Finding what packet has the player name is useful, because we can change
// it later:
if(properties.packets.foundName === false) {
var messageString = message.toString();
var namePacket = messageString.indexOf(properties.packets.name);

// When player name is found, it's index and position are saved:
if(namePacket >= 0) {
properties.data.name = properties.packets.name;
properties.data.nameIndexAt = namePacket;
properties.data.nameAppearsAt = properties.data.packets.length;

// Notify that the player name has been found:
console.log("<< SERVER >> Found player name at packet " + properties.data.nameAppearsAt + ".");
properties.packets.foundName = true;
}
}

// Count packets received (and toggle a "display" variable):
properties.packets.received += 1;
var display = (properties.packets.received % properties.packets.displayReceivedEvery == 0);

// Record packets (if enabled):
if(properties.packets.recordingEnabled === true) {
properties.data.packets.push(message.toJSON().data);
}

// Redirect message to VC:MP Server:
client.send(message, properties.vcmp.port, properties.vcmp.address, function (error) {
if(error) {
console.log("<< CLIENT >> << ERROR >>");
}
});
});

/**
 * Event called by server when it's listening for conections.
 */
server.on("listening", function () {
  var remote = server.address();
  console.log("[SERVER] Listening on \"" + remote.address + ":" + remote.port + "\"");
});

///////////////////////////////////////////////////////////////////////////////
// @file client.js
///////////////////////////////////////////////////////////////////////////////
/** UDP client. */
var client = dgram.createSocket("udp4");

/**
 * Event called by client when it receives a message.
 *
 * @param {Buffer} message Message received.
 * @param {Object} remote Conection information.
 */
client.on("message", function (message, remote) {
// Count packets sent (and toggle a "display" variable):
properties.packets.sent += 1;
var display = (properties.packets.sent % properties.packets.displaySentEvery == 0);

// If this conection is from VC:MP, it'll be sent back to client:
server.send(message, properties.client.port, properties.client.address, function (error) {
if(error) {
console.log("<< SERVER >> << ERROR >>");
}
});
});

///////////////////////////////////////////////////////////////////////////////
// @file index.js
///////////////////////////////////////////////////////////////////////////////
// Listen server...
server.bind(properties.server);

/**
 * Event called when application is closed.
 */
process.on("SIGINT", function () {
// Save packets to a file (if enabled):
if(properties.packets.recordingEnabled) {
var file = fs.writeFileSync(properties.data.name + ".json", JSON.stringify(properties.data));
}

// Notify that the application is being closed:
console.log("Application closed (" + properties.packets.received + " packets received; " + properties.packets.sent + " packets sent).");
process.exit(0);
});
[/noae][/noae]

Code used for playing packets (name it "play.js"):
[noae][noae]///////////////////////////////////////////////////////////////////////////////
// @file modules.js
///////////////////////////////////////////////////////////////////////////////
/** Arguments passed though command line. */
var args = process.argv.slice(2);

/** Filesystem module. */
var fs = require("fs");

/** Datagram module (used for UDP conections). */
var dgram = require("dgram");

/** Properties and settings. */
var properties = {
  // Server (VC:MP).
"vcmp": {
"address": "localhost",
"port"   : 7777 // <- VC:MP SERVER PORT
},

// Packet options.
  "packets": {
"counter"      : 0,
    "sent"         : 0,
"total"        : 0,
"name"         : "Default",
"replacedName" : false,
"intervalSpeed": 50
  },

// Saved data.
  "data": JSON.parse(fs.readFileSync(args[0] + ".json", "utf8"))
};

// Salve total of packets available and change player name:
properties.packets.total = properties.data.packets.length;
properties.packets.name = args[1] || properties.data.name;

///////////////////////////////////////////////////////////////////////////////
// @file client.js
///////////////////////////////////////////////////////////////////////////////

/** UDP client. */
var client = dgram.createSocket("udp4");

/** Interval used to send packets to VC:MP Server. */
var interval = setInterval(function () {
// Loop through after reaching the last available packet:
if(properties.packets.counter > (properties.packets.total - 1)) {
properties.packets.counter = 0;
}

// Get next packet to be sent:
var message = properties.data.packets[properties.packets.counter];

// If this is the packet where player name appears, it will be replaced:
if(properties.packets.replacedName === false && properties.packets.counter == properties.data.nameAppearsAt) {
// Iterate though characters...
for(var i = 0; i <= properties.data.name.length; i++) {
var char = properties.packets.name.charCodeAt(i);

// If new name is shorter than the previous one, set null string
// terminator and finish:
if(isNaN(char) === true) {
message[properties.data.nameIndexAt + i] = 0;
break;
}

// Replace character...
message[properties.data.nameIndexAt + i] = char;
}

// This name only needs to be replaced once:
properties.packets.replacedName = true;
}

// Send packet to VC:MP Server:
client.send(Buffer.from(message), properties.vcmp.port, properties.vcmp.address, function (error) {
if(error) {
console.log("<< CLIENT >> << ERROR >>");
}
});

// Increment total packets sent and counter:
properties.packets.sent += 1;
  properties.packets.counter += 1;
}, properties.packets.intervalSpeed);
[/noae][/noae]

For recording, use: node record.js <playername>
For playing, use: node play.js <filename> <playername>

Make sure to change some values in the "properties" variable before using it.

I managed to record and then spawn bots driving cars around the city. Since I made it to loop, these bots will quit and then come back to server to do the same thing all over again.



It just makes things like scripting a server so much more enjoyable to test in my opinion, even if these bots don't do anything useful. There's a huge difference between test a server alone or with 2 or 3 players and with a server full of people running around and preventing the city from feeling empty.

We could do a lot more if we knew the structure of packets, though. This would help a lot, specially for things like hit detection (deal and take damage from bots) and movement.

It's kinda funny to see how NPCs exist in other multiplayers, like MTA:SA, but nobody uses them, yet VC:MP, which could benefit from it to compensate low player counts, is the only GTA multiplayer mod which doesn't have this feature yet. Hopefully we can at least find workarounds like these to see how it would be like to having them.
#7
Snippet Showroom / Open IMG files
Apr 21, 2020, 03:35 AM
This script will let you open and dump content inside IMG files.
[noae][noae]/**
 * @class DirectoryArchive
 *
 * @description
 * Represents one DIR file, generally it's "gta3.dir".
 */
class DirectoryArchive {
  /** Table of Contents (TOC) of this DIR file. */
  toc = {};

  /**
   * @constructor
   *
   * @param {string} path File path.
   */
  constructor(path) {
    // File handler (the actual file object).
    local handler = file(path, "rb");

    // Iterate through DIR file...
    for(local i = 0; i < handler.len(); i += 32) {
      // File offset, size and name.
      local offset = handler.readn('i');
      local size   = handler.readn('i');
      local name   = "";

      // This will change to "true" after a string terminator is found...
      local isNameWritten = false;

      // Iterate through file name characters...
      for(local j = 0; j < 24; j++) {
        local char = handler.readn('b');

        // Ignore string terminator:
        if(char != 0x0 && isNameWritten == false) {
          name += format("%c", char);
        }

        // When it's found, changing this value to "true" will skip the rest.
        // This is done to align the seek:
        else {
          isNameWritten = true;
        }
      }

      // Save data information to it's TOC:
      this.toc[name] <- {
        offset = offset,
        size   = size,
        name   = name
      };
    }

    // After everything is done, close the file:
    handler.close();
  }
}

/**
 * @class ImageArchive
 *
 * @description
 * Represents one IMG file, generally it's "gta3.img".
 */
class ImageArchive {
  /** File path. */
  path = null;

  /** Directory archive. */
  directoryArchive = null;

  /**
   * @constructor
   *
   * @param {string} path File path.
   * @param {DirectoryArchive} directoryArchive Directory archive.
   */
  constructor(path, directoryArchive) {
    this.path = path;
    this.directoryArchive = directoryArchive;
  }

  /**
   * Dump one file inside of image file.
   *
   * @param {string} name File name.
   *
   * @return {blob} The requested file data.
   */
  function dump(name) {
    // Get the contents (if exists into directory archive's TOC)...
    if(name in this.directoryArchive.toc) {
      local contents = this.directoryArchive.toc[name];

      // File offset, size and name.
      local offset = contents.offset * 2048;
      local size   = contents.size * 2048;
      local name   = contents.name;

      // File handler (the actual file object).
      local handler = file(this.path, "rb");
            handler.seek(offset);

      // Requested file data.
      local data = handler.readblob(size);

      // After everything is done, close the file and return file data:
      handler.close();
      return data;
    }
  }
}
[/noae][/noae]
Here's an example. This will dump one DFF file and save it on server folder:
[noae][noae]// "gta3.dir" and "gta3.img" (these files must be into server folder):
local gta3dir = DirectoryArchive("gta3.dir");
local gta3img = ImageArchive("gta3.img", gta3dir);

// Taking one DFF object as example, this will return a blob object containing
// all it's data from the "gta3.img":
local dff = gta3img.dump("doontoon03.dff");

// Now all you need to do is to create one file and write to it:
local dffSave = file("doontoon03.dff", "wb");
      dffSave.writeblob(dff);
      dffSave.close();
[/noae][/noae]
There aren't many uses for this, though. The idea was to look around the map and dump the objects you wanted as you go, but that wouldn't work. And it's not like you couldn't use a map editor or IMG tool already, but that's another option anyway.

You can use this to take a look on how to read and write files in Squirrel if you need, too.
#8
Vice2D


Vice2D is a game library made for VC:MP. It should provide enough features for manipulating GUISprites and GUILabels, including rotation, position, collision detection and moving towards angle or position, and more.

Unfortunately, this library is just too big to be posted here directly, so I had to put it somewhere else. You can find a link for it right below (complete with syntax highlight):

Vice2D: A minimalist, event-based game library for VC:MP

Examples:


Hello, world


This will create a simple sprite on top-left position of screen. Use arrow keys to move the sprite. Link to image used by this example can be found written in the code.
[noae][noae]// Create core...
local game = Vice2D.Core().setDefaultKeys();

//
// Create spritesheet and image...
// A link to the image file is provided below:
//
// https://i.ibb.co/8dVGb3v/spritesheet.png
//
// To understand why the last values are 0.25 and 0.2:
//
// 256 / 64 = 4 (number of columns)
// 320 / 64 = 5 (number of rows)
//
// 1 / 4 = 0.25 (that's the uvX)
// 1 / 5 = 0.2 (that's the uvY)
//
local sst_player = Vice2D.Spritesheet(256, 320, 64, 64, 0.25, 0.2);
local spr_player = Vice2D.Image("spritesheet.png", sst_player);

// Create sprite...
local obj_player = Vice2D.Sprite(64, 64);
      obj_player.setImage(spr_player);
      obj_player.setFrames([0, 1, 2, 3]);

/**
 * @event update
 */
obj_player.addEventListener("update", function() {
  local speed = 4;
  if(this.input.held("up")   ) { this.y -= speed; }
  if(this.input.held("down") ) { this.y += speed; }
  if(this.input.held("left") ) { this.x -= speed; }
  if(this.input.held("right")) { this.x += speed; }
});

// Add player to core:
game.push(obj_player);

/**
 * @event Script::ScriptProcess
 */
function Script::ScriptProcess() {
  game.loop();
}

/**
 * @event KeyBind::OnDown
 *
 * @param {instance} key Key pressed.
 */
function KeyBind::OnDown(key) {
  game.input.read(key, true);
}

/**
 * @event KeyBind::OnUp
 *
 * @param {instance} key Key released.
 */
function KeyBind::OnUp(key) {
  game.input.read(key, false);
}
[/noae][/noae]

Arcade game


This one is a little more interesting. It was made to show a little of what this library can do. Use arrow keys to move the character and space bar to attack. Links to images used by this example can be found written in the code.
[noae][noae]// This is the core. All sprites must be added here.
local game = Vice2D.Core().setDefaultKeys();
      game.canvas.setViewport(0, 0, 512, 512);
      game.canvas.centerViewport();

// Fonts are set here...
local fon_counter    = Vice2D.Font(null, 192);
      fon_counter.AddFlags(GUI_FFLAG_BOLD | GUI_FFLAG_OUTLINE | GUI_ALIGN_CENTERH | GUI_ALIGN_CENTERV);
      fon_counter.Colour     = Colour(0, 0, 0);
      fon_counter.TextColour = Colour(83, 194, 113);
      fon_counter.Size       = VectorScreen(game.canvas.viewport.width, game.canvas.viewport.height);

// Spritesheets are set here...
local sst_background = Vice2D.Spritesheet(512, 512, 512, 512,     1,     1);
local sst_sword      = Vice2D.Spritesheet(768, 128, 128, 128, 0.166,     1);
local sst_particles  = Vice2D.Spritesheet(640, 128, 128, 128,   0.2,     1);
local sst_characters = Vice2D.Spritesheet(512, 384,  64,  96, 0.125,  0.25);

//
// Images are set here...
// A link to each image file is provided below:
//
// https://i.ibb.co/TcW2zRR/background.png
// https://i.ibb.co/MR0B8hw/sword.png
// https://i.ibb.co/7NSMm0g/characters.png
// https://i.ibb.co/Ntk18KQ/particles.png
//
local spr_background = Vice2D.Image("background.png", sst_background);
local spr_sword      = Vice2D.Image("sword.png",           sst_sword);
local spr_player     = Vice2D.Image("characters.png", sst_characters);
local spr_devil      = Vice2D.Image("characters.png", sst_characters);
local spr_skull      = Vice2D.Image("characters.png", sst_characters);
local spr_wizard     = Vice2D.Image("characters.png", sst_characters);
local spr_stone      = Vice2D.Image("characters.png", sst_characters);
local spr_particles  = Vice2D.Image("particles.png",   sst_particles);

/**
 * @class Particles
 * @extends Vice2D.Sprite
 */
class Particles extends Vice2D.Sprite {
  /**
   * @constructor
   */
  constructor() {
    base.constructor(128, 128);
    this.setImage(spr_particles);
    this.setFrames([0, 0, 1, 2, 3, 4]);
    this.setFrameSpeed(0);
    this.setVisible(false);
  }

  /**
   * Show particles on a specific position.
   *
   * @param {number} x X position.
   * @param {number}y Y position.
   */
  function show(x, y) {
    this.setFrame(1);
    this.setPosition(x, y);
    this.setFrameSpeed(0.5);
    this.setVisible(true);
  }

  /**
   * @event update
   */
  function update() {
    if(this.getFrameIndex() == 0) {
      this.setFrameSpeed(0);
      this.setVisible(false);
    }
  }
}

// These particles will appear when one of characters dies.
local obj_particles = Particles();

// Player's sword. It will be used later in the code...
local obj_sword = Vice2D.Sprite(128, 128);
      obj_sword.setImage(spr_sword);
      obj_sword.setFrames([0, 1, 2, 3, 4]);
      obj_sword.setFrameSpeed(0.5);
      obj_sword.hitbox.top = 64;

/**
 * @class Character
 * @extends Vice2D.Sprite
 */
class Character extends Vice2D.Sprite {
  /** Movement speed. */
  speed = 2;

  /** Mark true if you want to control the character. */
  playable = false;

  /** Determine character movement (true if running, false if idle). */
  running = false;

  /** If it's not playable, set a target to chase. If it is, set the sword. */
  target = null;

  /** Determine if character is dead or alive. */
  dead = null;

  /** Respawn time (-0.01). */
  respawnTime = 1.0;

  /** Enemies that may hurt the character. */
  enemies = [];

  /** Determine if character is attacking. */
  attacking = false;

  /** Cooldown time to attack. The player will not inflict any damage while on cooldown. */
  attackCooldown = 0.0;

  /** Respawn protection. The character will not take any damage. */
  protectionCooldown = 0.0;

  /** Player score. */
  score = 0;

  /**
   * @constructor
   *
   * @param {Vice2D.Image} image Character image.
   * @param {number} frames Animation frames.
   * @param {number} frameSpeed Animation speed.
   * @param {number} speed Movement speed.
   * @param {boolean} playable Mark true if you want to control the character.
   * @param {Vice2D.Sprite} target If it's not playable, set a target to chase. If it is, set the sword.
   */
  constructor(image, frames, frameSpeed, speed, playable, target) {
    base.constructor(64, 96);

    this.setImage(image);
    this.setFrames(frames);
    this.setFrameSpeed(frameSpeed);

    this.hitbox.top = 88;
    this.speed      = speed;
    this.playable   = playable;
    this.target     = target;

    this.spawn();
  }

  /**
   * Prevent character from moving out of bounds.
   */
  function stayWithinBoundaries() {
    // Left side of the boundary:
    if(this.x < 0) {
      this.x = 0;
    }

    // Right side of the boundary:
    else if(this.x + this.width > this.canvas.viewport.width) {
      this.x = this.canvas.viewport.width - this.width;
    }

    // Above the boundary (giving space for walls):
    if(this.y < -32) {
      this.y = -32;
    }

    // Below the boundary (giving space for walls):
    else if(this.y + this.height > (this.canvas.viewport.height - 64)) {
      this.y = (this.canvas.viewport.height - 64) - this.height;
    }
  }

  /**
   * Kills the character.
   */
  function die() {
    obj_particles.show(this.x - 32, this.y);

    this.setVisible(false);
    this.setPosition(0, 0);

    this.dead = true;
    this.respawnTime = 1.0;
    this.protectionCooldown = 1.0;
  }

  /**
   * Controls the events after the character's death.
   */
  function handleDeath() {
    // Control respawn time...
    if(this.respawnTime > 0.0) {
      this.respawnTime -= 0.01;
    }

    // Respawn character on a random location:
    else {
      this.setVisible(true);
      this.spawn();
      this.dead = false;
      this.protectionCooldown = 1.0;
      this.score = 0;
    }
  }

  /**
   * Spawn the character.
   */
  function spawn() {
    // The player always spawn on center of the arena...
    if(this.playable) {
      local center = this.canvas.getViewportCenter();
      this.setPosition(center.x - (this.width / 2), center.y - (this.height / 2) - 32);
    }

    // Enemies will respawn on random positions of the arena...
    else {
      local spawnX = (1.0 * this.canvas.viewport.width  * rand() / RAND_MAX).tointeger();
      local spawnY = (1.0 * this.canvas.viewport.height * rand() / RAND_MAX).tointeger();
      local speed  = (1.0 * 6.0 * rand() / RAND_MAX);

      if(speed <= 1.0) {
        speed = 1.0;
      }

      this.speed = speed;
      this.setPosition(spawnX, spawnY);
    }
  }

  /**
   * Controls the blinking effect while on spawn protection.
   */
  function handleSpawnProtection() {
    if(this.protectionCooldown > 0.0) {
      this.protectionCooldown -= 0.01;
      this.setVisible(this.frame % 2 == 0);
    }
    else {
      this.setVisible(true);
    }
  }

  /**
   * Controls attacks and position of player's sword.
   */
  function handleSword() {
    // This function can't continue without the target (sword):
    if(this.target == null) {
      return false;
    }

    // Mirror sword according to the player:
    this.target.setMirrored(this._mirrored);
    this.target.setVisible(true);

    // Position, animate and handle sword when it's being used...
    if(this.attacking) {
      this.target.setPosition(this.x, this.y);
      this.target.setFrames([1, 2, 3, 4]);
      this.target.setFrameSpeed(0.5);
      this.target.x -= this._mirrored? 64: 0;
    }

    // Stop sword when the player is not attacking...
    else {
      this.target.setPosition(this.x - 32, this.y - 32);
      this.target.setFrames([0]);
      this.target.setFrameSpeed(0.0);
      this.target.setFrame(0);
    }

    // Control attack cooldown:
    if(this.attackCooldown > 0.0) {
      this.attackCooldown -= 0.1;
    }

    // The sword will only deal damage on 2 specific frames...
    this.target.setCollisionsEnabled(this.target.frame == 2 || this.target.frame == 3);
  }

  /**
   * Handle controls for playable character.
   */
  function handlePlayer() {
    // This value will change if the character moves:
    this.running = false;

    // Move up...
    if(this.input.held("up")) {
      this.running = true;
      this.y -= this.speed;
    }

    // Move down...
    if(this.input.held("down")) {
      this.running = true;
      this.y += this.speed;
    }

    // Move left...
    if(this.input.held("left")) {
      this.running = true;
      this.setMirrored(true);
      this.x -= speed;
    }

    // Move right...
    if(this.input.held("right")) {
      this.running = true;
      this.setMirrored(false);
      this.x += speed;
    }

    // Attack...
    this.attacking = this.input.held("space");

    // Check for collisions...
    foreach(index, value in this.enemies) {
      if(this.intersect(value) && !this.dead && !value.dead && this.protectionCooldown <= 0.0) {
        this.target.setVisible(false);
        this.die();
        break;
      }
    }
  }

  /**
   * Handle controls for enemies.
   */
  function handleAI() {
    // Since this function does involve chasing a player, it needs a target to continue:
    if(this.target == null) {
      return false;
    }

    // Chase and look at the direction of player:
    if(!this.target.dead) {
      this.moveTo(this.target.x, this.target.y, this.speed);
      this.setMirrored(this.x > this.target.x);
    }

    // Check for collisions...
    if(this.intersect(this.target.target) && !this.dead && !this.target.dead && this.target.attackCooldown <= 0.0) {
      this.target.attackCooldown = 1.0;
      this.target.score += 1;
      this.die();
    }
  }

  /**
   * @event update
   */
  function update() {
    // Handle respawn if it's dead:
    if(this.dead) {
      this.handleDeath();
      return false;
    }

    // Keep character inbounds:
    this.stayWithinBoundaries();

    // Events for player...
    if(this.playable) {
      this.handleSword();
      this.handleSpawnProtection();
      this.handlePlayer();
    }

    // Events for enemies...
    else {
      this.handleAI();
    }
  }
}

// Background (arena)...
local obj_background = Vice2D.Sprite(512, 512).setImage(spr_background);

// Score counter...
local lab_counter = Vice2D.Label([fon_counter], "0");
      lab_counter.setPosition(0, -32);

// The player (the sword is passed as a target)...
local obj_player = Character(spr_player,  [0, 1, 2, 3, 4, 5, 6, 7], 0.2, 2, true, obj_sword);

// The enemies...
local obj_devil  = Character(spr_devil,   [ 8,  9, 10, 11, 12, 13, 14, 15], 0.2,   1, false, obj_player);
local obj_skull  = Character(spr_skull,   [16, 17, 18, 19, 20, 21, 22, 23], 0.2, 1.5, false, obj_player);
local obj_wizard = Character(spr_wizard,  [24, 25, 26, 27]                , 0.2,   2, false, obj_player);
local obj_stone  = Character(spr_stone,   [28, 29, 30, 31]                , 0.2, 2.5, false, obj_player);

// Let the player know who are the enemies so we can check collisions against them:
obj_player.enemies = [obj_devil, obj_skull, obj_wizard, obj_stone];

// The background is added first. We want it to appear below all the other objects.
game.push(obj_background);

// The score counter is added later. Characters will run above it, making it
// look like it's part of the floor.
game.push(lab_counter);

// Then, we add the enemies. They will chase the player around the arena.
game.push(obj_devil);
game.push(obj_skull);
game.push(obj_wizard);
game.push(obj_stone);

// Add the sword and the player.
game.push(obj_sword);
game.push(obj_player);

// And finally, we add the particles.
game.push(obj_particles);

/**
 * @event update
 */
game.addEventListener("update", function() {
  lab_counter.setText(obj_player.score.tostring());
});

/**
 * @event Script::ScriptProcess
 */
function Script::ScriptProcess() {
  game.loop();
}

/**
 * @event KeyBind::OnDown
 *
 * @param {instance} key Key pressed.
 */
function KeyBind::OnDown(key) {
  game.input.read(key, true);
}

/**
 * @event KeyBind::OnUp
 *
 * @param {instance} key Key released.
 */
function KeyBind::OnUp(key) {
  game.input.read(key, false);
}
[/noae][/noae]

The game looks like this:


Documentation:


All comments in the code are written in portuguese. Documentation is available in HTML and can be found on the links below.

Vice2D documentation in portuguese
Vice2D documentation in english (Google Translate)

Why?


After playing around with client-side scripting, I was thinking if wouldn't be nice to have effects and animations on screen when something happens during the game.

Sure, you can place some GUI elements here and there and you'll have a custom HUD. But what about the details? Imagine having a weapon selection where icons scale when you change a weapon, a firework effect when you win a prize, or even a new radar with animated blips! Unfortunately, GUI elements don't really look like they were intended to be moved around like that, and their styles don't seem to fit very well with what I was wanting to do. So I decided to look for another way of doing it.

Then, I decided to write a game library for VC:MP and see what could be done with it.

Uses


Although is not perfect, you should be able to create some cool games with it. Examples of games include casinos (poker, blackjack, slot machines, etc), arcade games (to play on the arcade machines inside Kaufman Cabs and pizza restaurants) or just extra mechanics for your gamemode.

Effects and animations, of course, are also possible. Take a little time to learn, throw some sprites into a core and just play around with it!
#9
Client Scripting / Animated GUISprites
Mar 31, 2020, 02:11 PM
Animated GUISprites

These functions can be used to create spritesheets and set animation frames to a GUISprite. Includes example and explanation below.
/**
 * Create a spritesheet.
 *
 * @param {number} imageWidth Image width.
 * @param {number} imageHeight Image height.
 * @param {number} width Frame width.
 * @param {number} height Frame height.
 * @param {number} x Horizontal unit position (from 0.0 to 1.0).
 * @param {number} y Vertical unit position (from 0.0 to 1.0).
 *
 * @return {table[]}
 */
function createSpritesheet(imageWidth, imageHeight, width, height, x, y) {
  // Spritesheet to be returned:
  local spritesheet = [];

  // Rows and columns of spritesheet:
  local rows    = imageHeight / height;
  local columns = imageWidth / width;

  // Frame index counter:
  local index = 0;

  // Iterate through rows and columns...
  for(local row = 0; row < rows; row += 1) {
    for(local column = 0; column < columns; column += 1) {
      spritesheet.push({
        // Frame index.
        index = index,

        // Initial cut position (unit value).
        topLeft = {
          x = x * column,
          y = y * row
        },

        // Final cut position (unit value).
        bottomRight = {
          x = (x * column) + x,
          y = (y * row) + y
        }
      });

      index += 1;
    }
  }

  return spritesheet;
}

/**
 * Set animation frame for one image.
 *
 * @param {GUISprite} image Image.
 * @param {table} frame Animation frame.
 */
function setSpriteFrame(image, frame) {
  image.TopLeftUV.X = frame.topLeft.x;
  image.TopLeftUV.Y = frame.topLeft.y;

  image.BottomRightUV.X = frame.bottomRight.x;
  image.BottomRightUV.Y = frame.bottomRight.y;
}

Example

This will animate a GUISprite using this image (name it "spritesheet.png"): https://i.ibb.co/8dVGb3v/spritesheet.png
// Animated sprite. It does have a size of 256x320:
local sprite_anim = GUISprite("spritesheet.png", VectorScreen(0, 0));
sprite_anim.Size = VectorScreen(128, 128);

// Create spritesheet:
local spritesheet = createSpritesheet(256, 320, 64, 64, 0.25, 0.2);
setSpriteFrame(sprite_anim, spritesheet[0]);

// Animation control:
local sprite_frame = 0;
local sprite_delay = 0;

/**
 * @event Script::ScriptProcess
 */
function Script::ScriptProcess() {
  // Increase delay counter...
  sprite_delay += 1;

  // Change the frame and reset the delay:
  if(sprite_delay >= 20) {
    sprite_delay = 0;

    // Set and advance frame:
    setSpriteFrame(sprite_anim, spritesheet[sprite_frame]);
    sprite_frame += 1;

    // Loop frames:
    if(sprite_frame >= spritesheet.len()) {
      sprite_frame = 0;
    }
  }
}

How it works?

While GUISprites are typically used to display one full-sized image, it's possible to display only one part of it, too. If used alongside a spritesheet or texture atlas, this can be used for a lot of things, like animations. This is achieved by using "TopLeftUV" and "BottomRightUV". So how does that work?

Basically, these properties determine where your image starts and ends. When a GUISprite is created, the default properties are always set to display the whole image. The first position (the top-left) will always be at "0,0", and the last position (the bottom-right) will always be at "<width of image - 1>,<height of image - 1>".

Let's take the "logo.png" from Blank Server, for example:



The image size is 500x247, so the top-left position will be at "0,0" and the bottom-right position will be at "499,246". Simple enough, right?

Well, not really. Take a look at this:
// This is the logo image from Blank Server (500x247):
sprite_logo <- GUISprite("logo.png", VectorScreen(0, 0));

// Top-left position: 0,0 (as expected).
Console.Print("Top-left position: " + sprite_logo.TopLeftUV.X + "," + sprite_logo.TopLeftUV.Y);

// Bottom-right position: 1,1 (wait... what?).
Console.Print("Bottom-right position: " + sprite_logo.BottomRightUV.X + "," + sprite_logo.BottomRightUV.Y);

What went wrong?

Turns out the "UV" properties don't use pixels, but percentages! These properties will go from 0.0 (0%) to 1.0 (100%) and can wrap or flip the image if you go lower or greater than these values. This can give you interesting effects, like a moving background.

We can also take advantage of percentages to make simple cuts. Let's say we want to display only half of the logo:

// This is the logo image from Blank Server (500x247):
sprite_logo <- GUISprite("logo.png", VectorScreen(0, 0));
sprite_logo.BottomRightUV.X = 0.5; // This will remove 50% of the right side.

// The image has been cut, but the original size of the GUISprite will
// remain the same. We need to resize it too if we don't want the result
// to appear stretched:
sprite_logo.Size.X = 250;

Remember to resize the image, too. You should see something like this:



Spritesheets

Now let's move onto spritesheets. Take this image, and name it "spritesheet.png":



The image size is 256x320, but every frame is 64x64. Just to get started, let's split this into rows and columns and see if we can figure something out:



We have 5 rows (20% per row) and 4 columns (25% per column). If we want to display the frame where the character is scared, we need to move the top-left position to "25%,80%" and the bottom-right position to "50%,100%".

// This is the sprite (256x320):
sprite_anim <- GUISprite("spritesheet.png", VectorScreen(0, 0));
sprite_anim.Size = VectorScreen(64, 64);

// Set the top-left position:
sprite_anim.TopLeftUV.X = 0.25;
sprite_anim.TopLeftUV.Y = 0.8;

// Set the bottom-right position:
sprite_anim.BottomRightUV.X = 0.5;
sprite_anim.BottomRightUV.Y = 1.0;

And that's how it's done.
#10
Script Showroom / Simple Garage System
Mar 24, 2020, 07:56 PM
VC:MP doesn't offer Pay n' Sprays and garages by default, you'll need to make them yourself. This, however, can be a quite difficult thing to do if you're just getting started, because all doors are closed too. Objects can be removed by using HideMapObject(), but it's still necessary to look for IDs and positions on the map.

Unfortunately, I couldn't find anything. And with no object IDs and positions available, the only option is to get a map editor and look for them by yourself.

I found this very annoying, specially because some servers already have this and it should be something really simple to do. So after going through all this, I decided to make a script to do just that: opening and closing garages. This should be a easy way to get started.
[noae]/**
 * @class Garage
 */
class Garage {
  /** Available garages. */
  static list = {

    /** Pay n' Spray (Little Haiti). */
     haiwshpnsdoor = {
      ID = 1573,
      Pos = Vector(-874.696, -116.695, 11.987),
      OpenPos = Vector(-874.696, -116.695, 15.987),
      Speed = 2000,
      SoundID = 176
    },

    /** Pay n' Spray (Viceport). */
     dk_paynspraydoor = {
      ID = 828,
      Pos = Vector(-910.001, -1264.709, 12.492),
      OpenPos = Vector(-910.001, -1264.709, 16.492),
      Speed = 2000,
      SoundID = 176
    },

    /** Pay n' Spray (Washington Beach). */
     wshpnsdoor = {
      ID = 3013,
      Pos = Vector(-7.804, -1257.642, 10.818),
      OpenPos = Vector(-7.804, -1257.642, 15.818),
      Speed = 2000,
      SoundID = 176
    },

    /** Pay n' Spray (Vice Point). */
     nbecpnsdoor = {
      ID = 4145,
      Pos = Vector(325.083, 431.136, 11.587),
      OpenPos = Vector(325.083, 431.136, 16.587),
      Speed = 2000,
      SoundID = 176,
      Rotation = Vector(0, 0, 2.810)
    },

    /** Bomb Shop (Viceport). */
    dk_bombdoor = {
      ID = 837,
      Pos = Vector(-1161.317, -1402.474, 12.798),
      OpenPos = Vector(-1161.317, -1402.474, 17.798),
      Speed = 2000,
      SoundID = 176
    },

    /** Import/Export (Sunshine Autos). */
    lh_showdoor1 = {
      ID = 1392,
      Pos = Vector(-975.454, -841.796, 8.159),
      OpenPos = (-975.454, -841.796, 12.159),
      Speed = 2000,
      SoundID = 176
    },

    /** Pay n' Spray (Sunshine Autos). */
    lh_showdoor03 = {
      ID = 1393,
      Pos = Vector(-1007.945, -841.778, 8.594),
      OpenPos = Vector(-1007.945, -841.778, 12.594),
      Speed = 2000,
      SoundID = 176
    },

    /** Tank door (Little Havana). */
     lhtankdoor = {
      ID = 1428,
      Pos = Vector(-1054.399, -478.073, 12.525),
      OpenPos = Vector(-1054.399, -478.073, 16.525),
      Speed = 2000,
      SoundID = 176
    },

    /** Garage #1 (Sunshine Autos). */
     hav_garagedoor1 = {
      ID = 1388,
      Pos = Vector(-988.085, -821.650, 8.512),
      OpenPos = Vector(-988.085, -821.650, 12.512),
      Speed = 2000,
      SoundID = 176
    },

    /** Garage #2 (Sunshine Autos). */
    hav_garagedoor02 = {
      ID = 1395,
      Pos = Vector(-998.851, -821.650, 8.580),
      OpenPos = Vector(-998.851, -821.650, 12.580),
      Speed = 2000,
      SoundID = 176
    },

    /** Garage #3 (Sunshine Autos). */
    hav_garagedoor03 = {
      ID = 1396,
      Pos = Vector(-1010.188, -821.650, 8.580),
      OpenPos = Vector(-1010.188, -821.650, 12.580),
      Speed = 2000,
      SoundID = 176
    },

    /** Garage #4 (Sunshine Autos). */
    hav_garagedoor04 = {
      ID = 1397,
      Pos = Vector(-1021.867, -821.650, 8.566),
      OpenPos = Vector(-1021.867, -821.650, 12.566),
      Speed = 2000,
      SoundID = 176
    }

  }

  /**
   * Hide a garage.
   *
   * @param {string} name Garage name (see Garage.list).
   * @return {boolean}
   */
  static function hide(name) {
    if(name in Garage.list) {
      local garage = Garage.list[name];
      ::HideMapObject(garage.ID, garage.Pos.x, garage.Pos.y, garage.Pos.z);
      return true;
    }

    return false;
  }

  /**
   * Create a garage.
   *
   * @param {string} name Garage name (see Garage.list).
   * @return {boolean}
   */
  static function create(name) {
    if(name in Garage.list) {
      Garage.hide(name);

      local garage = Garage.list[name];
      local instance = ::CreateObject(garage.ID, 0, garage.Pos, 255);

      if("Rotation" in garage) {
        instance.RotateToEuler(garage.Rotation, 0);
      }

      return Garage(instance, garage.OpenPos, garage.Pos, garage.Speed, garage.SoundID);
    }

    return null;
  }

  /** Check if garage is open or closed. */
  _closed = true

  /** Garage object reference. */
  instance = null

  /** Garage position when it's open. */
  openPosition = Vector(0, 0, 0)

  /** Garage position when it's closed. */
  closedPosition = Vector(0, 0, 0)

  /** Opening and closing speed. */
  speed = 1000

  /** Sound played when a garage opens or closes. */
  soundID = 176

  /**
   * @constructor
   *
   * @param {instance} instance Garage object reference.
   * @param {Vector} openPosition Garage position when it's open.
   * @param {Vector} closedPosition Garage position when it's closed.
   * @param {number} speed Opening and closing speed.
   * @param {number} soundID Sound played when a garage opens or closes.
   */
  constructor(instance, openPosition, closedPosition, speed, soundID) {
    this._closed = true;
    this.instance = instance;
    this.openPosition = openPosition;
    this.closedPosition = closedPosition;
    this.speed = speed;
    this.soundID = soundID;
  }

  /**
   * Open this garage.
   *
   * @return {boolean}
   */
  function open() {
    if(this.instance != null && this._closed == true) {
      ::PlaySound(0, this.soundID, this.closedPosition);
      this.instance.MoveTo(this.openPosition, this.speed);
      this._closed = false;
      return true;
    }

    return false;
  }

  /**
   * Close this garage.
   *
   * @return {boolean}
   */
  function close() {
    if(this.instance != null && this._closed == false) {
      ::PlaySound(0, this.soundID, this.closedPosition);
      this.instance.MoveTo(this.closedPosition, this.speed);
      this._closed = true;
      return true;
    }

    return false;
  }

  /**
   * Switch between opening and closing the garage.
   *
   * @param {boolean}
   */
  function toggle() {
    if(this._closed == true) {
      this.open();
    }
    else {
      this.close();
    }

    return true;
  }

  /**
   * Check if garage is open or closed.
   *
   * @return {boolean}
   */
  function isClosed() {
    return this._closed;
  }
}
[/noae]
Let's say you want to use the Pay n' Sprays. All you'll need to do now is this:
[noae]// Available Pay n' Sprays.
PayNSprays <- {
LittleHaiti = Garage.create("haiwshpnsdoor"),
Viceport = Garage.create("dk_paynspraydoor"),
WashingtonBeach = Garage.create("wshpnsdoor"),
VicePoint = Garage.create("nbecpnsdoor"),
}

PayNSprays.LittleHaiti.open(); // Open the Little Haiti's Pay n Spray.
PayNSprays.LittleHaiti.close(); // Close the Little Haiti's Pay n Spray.
PayNSprays.LittleHaiti.toggle(); // Open if it's closed, close if it's open.

if(PayNSprays.LittleHaiti.isClosed()) {
  MessagePlayer("This Pay n' Spray is closed.", player);
}
[/noae]
It does have some flaws, though. Some garages are not well positioned and you may want to change some values if you find necessary (like the opening/closing sounds and the speed). Also, some IDs and positions are still missing, like the safehouse's garages. But this should be good enough for a quick start.

And if you want to set a new garage instead of just replacing the already existing ones, you can try something like this:
[noae]// Create a new object
randomObject <- CreateObject(1, Vector(0, 0, 0));

// Create the garage
randomGarage <- Garage(randomObject, Vector(0, 0, 10), Vector(0, 0, 0), 1000, 176);

// Open the garage
randomGarage.open();
[/noae]
#11
Reuploaded the file and updated the download link:
Download Link (reupload)
#12
Hello.

I just found VC:MP 0.4 Blank Server and decided to take a look at how to setup a VC:MP server. But while it's good enough for running locally and see if everything runs fine, I felt annoyed with some parts when I was trying to get started. Some of my thoughts:

  • All functions are listed, but there's no description of what they do or what type you expect the arguments to be;
  • I don't like Allman style indentation;
  • I don't like spaces between function arguments either;
  • Some functions are quite hard to understand. SendDataToClient(), for example, may look almost like "magic code" for beginners because of it's indentation issues and excessive use of one-liners;

I also feel that reading documentation in portuguese instead of english would be more comfortable for me, so I decided to look through all the code and try to document, comment and explain the best I could with this language in mind.

And here's my result:
Download Link (reupload)

Here are some of my changes:
  • All functions have documentation written above them. That includes @event tags (to tell what is an event and what isn't),  descriptions (some are vague, some are detailed) and a list of arguments (including it's types);
  • Code has been reindented to Ratliff/K&R style;
  • No more spaces between brackets;
  • I did my best to try understand how some of functions work and decided to comment everything;
  • Portuguese documentation is available in Markdown and HTML on "docs" folder;
  • /pos command added, because why not? It can be used initially to take some positions and play around with XML, learning how to spawn cars, objects, teleport functions, etc;
  • 80 columns per line;

There were at least 3 things I wanted to do, but didn't:
  • Change return 1 to return true;
  • Clean up argument types (some are IDs, some are instances... is hard to describe exactly unless when you try to use the function and find by yourself; not even the Wiki itself explains that very well);
  • Move all color codes to a property list. Color.Yellow seems way easier to understand than "[#FFFF81]";

Everything is written in portuguese, but I encourage anyone else to do the same in english as well. I know there's nothing special about adding a bunch of vague descriptions on top of functions, but it does help, specially for actual working code like SendDataToClient() and SendDataToServer(). When they're commented that way, people can see what everything does straight away. Some functions may also be confusing to differentiate, like onGameResize() and GUI::gameResize().

Some of my thoughs on Blank Server are mostly opinion-based. This is also  basically the same code, just commented out (and in another language).

Even though you don't understand portuguese, please give a look. I hope you like my try on "refactoring" Blank Server's code (more like "rewriting everything specifically for me to understand", but yeah...).

Blank Server is great for what's intended to be: a quick and easy way to test and start a server. It's just a little messy, but good enough to get you started. So thanks to Sebastian for providing it.