Effortless Text Localization

Posted in Methodology, Programming, Server Administration, Tips on September 29th, 2009 by Thomas
No Gravatar

Automatization of repeated work is one of the keys to productive development. Another is abstraction of common problems to allow concentration on project specific work. Localization is one of the problems you have in nearly every project, especially in iPhone projects. The iPhone SDK brings interesting solutions that abstract many parts of the localization process.

The tool ibtool, for example, extracts strings from an interface automatically. The strings are placed in a .strings file, a textfile with key/value pairs. To localize an interface, you have to translate .strings and merge the strings back into the interface again. Because you have individual interfaces for every localization, it’s possible to adjust widgets individually for each one.

genstrings is another tool inside the iPhone SDK. It extracts textIDs from the source code and write them into a .strings file. You may ask how the tool knows which texts need localization and which do not. The solution is the macro NSLocalizedString, which will be replaced by a .strings file lookup method by the preprocessor, but also searched for by the genstrings tool to create the files.

Both tools help you to create a localized application without paying much attention to localization itself. But you cannot expect the localization department to search for .strings files inside your project and create localized versions of them. Of course this would be possible, but not very convenient, because you have to migrate the translated texts back into the interfaces using ibtool. Another reason for us at Rough Sea is that we use a localization interface from our publisher. This interface is well known to the localization department and the content is placed in a centralized database on a server.

So we have the great tools from Apple that help us to separate texts from the project and we have the great tool from our publisher that handles the whole translation and reviewing process. Now we need something to tie those tools together, because we do not want to insert new texts from the .strings file into the publisher’s localization tool manually or vice versa. This glue tool has to execute the Apple tools, extract the texts from the .strings files and insert them into the publisher’s loca tool. On the other hand, it has to check for new localized texts from the publisher’s loca tool, build the required .strings files from the results and merge them back into the interfaces. Sounds quite easy, but of course there are some obstacles to get there. You have to handle other things, like the deletion of a text entry or changes to an already translated interface. So you have to know what has changed since the last update and stuff like that.

It turns out that you only have to integrate this glue tool into the build process of your build server. The tool will update the localization database whenever the code or the interface changes and it will update the localized versions when the database changes. As a coder you only need to remember to use the text macro around your text id. You don’t have to add this text id in a file or anything else. As you commit your changes, the build server will do this for you. As an interface designer it’s the same: just create your interfaces in the primary language and commit it. After the localization department finishes localizing those texts, they will be inserted into the localized versions automatically. Of course you have to make adjustments to the interface if there are loca bugs like labels that are to small to hold the translated text.

As you can see those localization tools are a big black box for coders, interface designers and translators. The coders only have to write code, the interface designers design interfaces and the translators translate texts. At the end there will be a localized product.

Popularity: 61% [?]

  • Share/Bookmark
Tags: , , , , , , , ,

Random Map Generation

Posted in Art, Programming, Tips on August 24th, 2009 by Manuel
No Gravatar

Designing an appealing and huge game world from scratch will take even really fast designers a looooong time: valuable time that can be spent in better ways. This is where generated content comes in handy. With the push of a button a designer can create a whole world (oceans, land, forests, deserts, mountains, cities, … ) in an instant. So this is what we decided to do for our game … and here is how we got to the finish line:

On the bumpy road to an actual working map generator we decided to split all map generation tasks into small modules: heightmap generator, water generator, forest generator, desert generator, and so on. In the map-generator GUI the designer can put these modules into a task list and define the order in which they are executed. Each module also has parameters that affect the characteristics of the generated map. Even though it is supposed to be a random map generator it is especially important to have deterministic behavior. Consequently, each module that actually generates content has at least a “seed” parameter for the random number generator. With the same seed the generator will create the same results every time. Changing the seeds will create a different map.

map generation steps
In the little animation above you can see the output of some of the modules.

Heightmap Generator
At first we create a heightmap. We use Perlin noise since it is built into the AS3 BitmapData class and it is amazingly fast. In the beginning we experimented with our own implementation of the diamond-square algorithm, but it was way too slow.

Water Fill
In the next step we apply water to the world. We simply declare everything below a certain height level as being under water.

Mountains
By looking for local peaks we detect mountains and flag the areas accordingly.

Humidity Map
By defining a water contingent that grows over water and decreases over land we can flag dry and wet zones on the continents. To make things easier we define our whole world as a west-wind zone. The centers of most bigger continents will be dry (yellow), coast regions will be mostly humid (green) and continental areas lying east of a big stretch of water will be very humid (dark green).

Climate Zones
Next we define climate zones in the world: tropical areas, temperate zones and polar regions. With this information we can easily place polar caps in the world.

Forest Generator
Using the information from the climate zones and the humidity map makes it possible to place different types of forests into the world. Jungle (purple) will be found in the hot and wet regions around the equator, while coniferous forests will be found close to the polar caps.

Desert Generator
The desert generator also uses the information from the humidity map to detect dry regions and place desert areas into them.

Well, now you had a look at some of the modules in our map generator. They are not only capable of creating earth-like maps (like the small sample map above) but also completely different maps by changing the parameters: a lake landscape, a waste land, a desert or whatever our games may need. With this we are able to quickly create new game content of any size for the players to explore.

Cheers,
Manuel

Popularity: 100% [?]

  • Share/Bookmark
Tags: , , , , , , , , , ,

R&D goes iPhone

Posted in Company, Programming, Tips on August 3rd, 2009 by Thomas
No Gravatar

Hello out there,

my name is Thomas and I am the first programmer in our new R&D department. Like most of the other guys here at Rough Sea, I’ve worked on games for consoles (Nintendo DS and Wii) and PC before. I’m very excited to develop sophisticated software that helps us to make state-of-the-art games. Of course this is only technically, but I’m pretty sure that Rafael and Jan will assure this for the design part too.

I assume you’ve read the headline already, so you know what the R&D department will work on in the coming months. We’ve decided to do some research on the iPhone, because we think the way you use the iPhone is almost the same as how you play a browsergame. You get your iPhone out of your pocket, use it for a few seconds and put it back again. You have nearly the same procedure with browsergames: you open your browsergame window/tab, change a few settings and go back to work.

So the first thing to do is getting your working environment running. The only legal way to do this is to buy a Mac. I’m not really happy about that, because I’m a Windows guy and there are some differences in using MacOS instead of Windows. Maybe Apple only invented the app store to sell some of their Macs to iPhone developers. Nevertheless after some adjustments the Mac became usable. The nextStep (pun indended) was the installation of the iPhone SDK and all the tools that belong to it. It’s quite easy to get from the installation of the SDK to the first Hello World on the simulator (especially if you compare this to console development).

We at Rough Sea use test driven development and continuous integration to assure that we always have  a deliverable product at hand. Unit Tests are already integrated into the SDK and it was easy for Ole to set up a build server on a Mac Mini with hudson and the makefile-like shell tool xcodebuild. You don’t have a makefile: the tool just uses the settings of the project file to know how to build. Another thing is that you can add and change text macros for the XCode IDE. Text Macros are very useful to write code much faster. For example, you can add an alloc-init call [[class alloc] init] simply by typing the letter a and then escape. There are macros for if, for and while too. Because we have different coding guidelines than the apple guys, I had to change some of the macros. You find them in /Developer/Applications/Xcode.app/Contents/ PlugIns/TextMacros.xctxtmacro/Contents/ Resources. Copy the TextMacros.xctxtmacro folder to Library/Application Support/Developer/Shared/Xcode/Specifications in your home directory. Just open the *.xctxtmacro files with a text editor to change the macros. You can add your own file and project templates too. This is very nice, because you can set up a default project with many settings, like Unit Test Targets, already in place, and use this project to create new ones with only one click. To add a new project template, create the project the way you like it and copy the whole project folder to Library/Application Support/Developer/Shared/XCode/Project Templates/GROUP_NAME/PROJECT_NAME. For file templates you should just copy the File Templates folder from /Developer/Library/Xcode/ to Library/Application Support/Developer/Shared/XCode/ and change or add the files you like.

Next time I will write about Objective-C and libraries from the iPhone SDK like UIKit.

Popularity: 80% [?]

  • Share/Bookmark
Tags: , , ,

Secure your Webserver (part 2- A)

Posted in Server Administration, Web security on July 20th, 2009 by Ole
No Gravatar

Hello everybody,

Finally you are able to read Part 2 of “Secure your Webserver”. Part 2 will be about Linux, Webserver and the other important services.

As I mentioned in Part 1, I will not write about Windows, MacOS or  other operating systems, because the most common one for a webserver  is Unix / Linux. Part 2 will have a part A and B. Today I am going to write about distributions,  user managament, user rights, secure shell and their enormous importance for security.

Before you start, choose the right distribution of Linux for your aims.  There are a lot of Linux versions (distributions) on the market. Most of them are free or have a free community edition. The most common free distributions are CentOS, Debian, Fedora, Gentoo, OpenSuse, Mandriva and Ubuntu. Of course there are also commercial destributions like Red Hat Linux Enterprises (RHEL) or Suse Linux Enterprises.

In general all these distributions are fine for a webserver. Their differences are minor and more a personal choice than a real technical question. A NEWBIE should maybe use OpenSUSE, Ubuntu or Fedora, as the support of the community seems to be bigger for them. Commercial products are usually superior to free distributions in their support system. They grant better support via phone, etc.

After you have chosen the right distrubtion for your purpose and installed it on your server machine, it is time to think about security.

1. User-Management and Shell-Access. (Secure Shell Daemon)

Linux security is mostly based on user rights. User rights are essential to the security concept of Unix and Linux systems. Usually all distributions are very strict and separate services (daemons), users, administrators, essential services like a webserver, MTA & MDA (Mail Transfer Agent & Mail Delivery Agent) into different groups and users.

All these groups and users have different write and read accesses. Usually groups are created to give a bunch of users the same rights. Therefore groups can make your administrator’s life far easier.

The most important “user” is called root. Root is the highest ranked user on a system. The “root user” has full read and write access. In the Windows world it would be the “Administrator”. This is the reason why it is not smart to use the root user in your everyday work. The root user should only be used for system critical and important parts. In all other cases it is wise to use a “normal” user, which you have created.
It is also possible to run root commands via your user account. Important commands for this purpose are “sudo” and “su”. Sudo runs a command line with a special user. The command su makes it possible to log in as another user via your own user shell. Of course you need the right password of the user to perform these actions.

X11_ssh_tunnellingIt is common for Linux to allow remote login via secure shell (SSH), especially if your webserver is not reachable for you in person, e.g. a dedicated server in a data centre of your webhost. All connections via SSH are encrypted. It is nearly impossible to decrypt the data via your client and your server (Maybe the NSA or the CIA are able to decrypt this – who knows?). You should take care to choose the ssh version 2 protocol. This is safer, as recently some weaknesses were discovered in protocoll version 1. A common SSH-client for Windows is “Putty”.

It is possible to permit or forbid login via SSH for specific users or user groups. Maybe it is not wise to allow direct root login via SSH. It is also possible to login via a certificate. You create an public and private key. Upload the public part to your server (usually: /homedir/.ssh/authorized) and log in without password. Of course it is wiser to secure your private key with a passphrase. In this case you need to type the phassphrase to decrypt the private key to log in.

Next part will be about firewalls, virusscanners and how to avoid spam problems.

Popularity: 39% [?]

  • Share/Bookmark
Tags: , , ,

Feel the groove

Posted in Programming, Tips on June 16th, 2009 by Joerg
No Gravatar

Our server code can be built with one click. The build process does not take long. To be precise, the build server says “last build: 1 minute 38 seconds”.  That’s cool. (And mainly it’s because the code is not that complex yet…)

But despite fast builds and because they are definitely going to get slower as the project progresses, we need the ability to change certain parts of the game logic with a minimum of work. So far this is mainly for the game designers, who want to be able to change some equations that influence the game’s balance. Defining stuff in config files that are read on server startup is nice, and we use this as well, but starting the server takes time, too. A better way is to be able to change game logic at run time.  So a scripting language was the way to go.

We decided to use groovy. Now groovy is not a scripting language at all, but more or less an object-oriented programming language that can be used as a scripting language for Java. Groovy has some advantages over Java like e.g. dynamic typing.

Unlike in other scripting languages, the groovy code is not interpreted at runtime but compiled into java byte code. So don’t forget to automatically reload the scripting files every now and then to make sure that changes in the files are actually applied.

We haven’t yet decided whether to keep the scripting in the release version of the game or not, since implementing the logic in the code is surely better for the performance. People on the Internet tend to accuse groovy of weak performance in comparison to other scripting languages.  But it is very pleasant for experimenting during development. If you don’t believe me, go ask our designers…

Popularity: 11% [?]

  • Share/Bookmark
Tags: ,

ByteArray Beats Regular Array

Posted in Methodology, Programming on April 21st, 2009 by Manuel
No Gravatar

To compare the performance of different approaches for handling large data I wrote a small test suite.

This application contains one test class (cDataHandlingTest) that writes to and reads from an abstract data class (iData). Each data class offers an unified interface but is implemented in different ways. The test class repeats each test several times, records initialization times and accessing times and then calculates the average for each data class. If you want to have a closer look on the code, you can download the FlashDevelop-project here. You can check out the test application at the end of the post.

Three-Dimensional Array (cDataArray3D)

This data class uses a three-dimensional array to store the data. Initialization takes a very long time because you need to cycle through several nested loops allocating the data. Access times are ok.

Linear Array (cDataArray1D)

Here I used a linear array for the data. Initialization for a linear area is lightning fast. The draw-back is the access, though. Calculating the offset of the data in the array is pretty complex:

var index:int = (((_y * m_width ) + _x) * m_entries) + _entryType;

This makes working with a linear array really slow if you cannot predict in what order you will access the data. If you will usually access the data in the exact order it is stored in the array, you would actually be really fast. Unfortunately, this is usually not the case. The linear array is about 10 times slower than the three-dimensional one on random access.

ByteArray Int-Based Access (cDataArrayByte)

In this example I used a ByteArray. The position is calculated in the exact same way as in the linear array example. Data is extracted by using a readInt() command on the stream, data is written by using the writeInt() command. I expected this approach to be really slow. Well it isn’t! On my computer it already outperforms the three-dimensional array in all areas. (It might not on others, though).

ByteArray Byte-Based Access (cDataArrayByteOpt)

If you only need values between 0 and 255 you can use the array operator [ ] of the ByteArray class. This is the fastest way to access the data. Initialization times are really good and access is on my computer about 25% faster than the three-dimensional array.

Test Application

This is the test. Click into the window to start. Be careful, it needs a lot of resources. If you have significantly less then 2GHz per core or only one core I recommend not to try it because it will lock up your computer for too long to be convenient.

Popularity: 8% [?]

  • Share/Bookmark
Tags: , , , , , , ,

Handling large data with AS3

Posted in Programming on March 30th, 2009 by Manuel
No Gravatar

We decided to do our map generator in Flash. One of the reasons was we can share the render code between game client and generator this way. We are planning on having really big maps in our game. To be on the safe side I quadrupled the requirements made by game design (well, you never know) and ended up with more than 10 million values to stuffed into memory. Each map position has different values such as terrain type, climate zone, humidity and height level, … so this is really a lot of data!

With such large data blocks I was facing three problems: One is data initialization time, another one is access time and the last one is memory footprint.

In the first iteration of my map-data class I used our extensive and comfortable dynamic property system. I thought it would be a good idea if I could add any amount of arbitrary data to any map entry. This freedom came with big price tag: It took forever to initialize and used about one Gigabyte of memory. This was absolutely not acceptable!

Consequently, this was not the way to go. Next, I tried a three-dimensional array. This approach makes the data a bit less flexible but it’s still ok. Initialization time was better but still not great. The memory footprint wasn’t what I would call good either. At least, I was now able to fit the data into memory (we’re still talking about over 100MB, though). To bring down initialization times I refactored the data class to use a linear array. Linear arrays are really fast to create, but then I realized that access times are terrible. So, I did not find a satisfying solution here either.

More or less by accident I stumbled over Flash’s built-in ByteArray class. It is basically works as a stream so I did not expect a great performance gain. I tried it anyway. Well, it really outperformed the three-dimensional array, which is astounding since the ByteArray needs the same offset calculations as the linear array does. The best thing about the ByteArray is it’s memory footprint. There is no class overhead and if you only need values between 0 and 255 (as I did) you can use one byte per value instead of wasting 4 bytes (size of int) each. This brought down the required memory to around 10 MB.

In my next post I will write about a little test suite I wrote to prove the power of the ByteArray. So stay tuned and remember to give ByteArray a chance.

Happy coding! :)

Popularity: 5% [?]

  • Share/Bookmark
Tags: , ,

State Machine Best Practices

Posted in Methodology, Programming, Uncategorized on January 22nd, 2009 by Manuel
No Gravatar

In game development, state machines are the most common way to structure the behavior of code.  Wikipedia defines a state machine as :

state machine is a model of behavior composed of a finite number of states, transitions between those states, and actions.

Probably every game programmer has worked with or even created a state machine that turned into debugging mayhem. The road there is short and simple: Game programmers usually do not bother with planning a state machine carefully before starting to code. They usually just create an object, add the obviously needed states and add more states and functionality as needed over time. After a while, the code is full of “set state” calls mingling the states and creating a most complex structure, whose behaviour becomes almost impossible to predict.

We believe in the DRY principle propagated in the book “The Pragmatic Programmer” by Andrew Hunt and David Thomas. DRY is short for “Don’t Repeat Yourself”.  It simply means that duplication of information should be avoided. Creating a state-machine diagram and an implementation is an duplication of information already.

How can the DRY principle be applied to state machines?

Our answer is: Include the state-machine diagram in the code! Using such an in-code diagram makes it really easy to understand how an object works and how states are connected. This transparency is gained by triggering the state changes through observing changes in the internal data rather than using external stimuli. To make this possible we completely eliminated the use of the “set state” method (except for setting the initial state of an object).

Let’s get practical: We declare our state-machines in the constructor of our objects. First, we define all the needed states. States consist of a state method (reference to a member function) and a state id (constant integer value). Each state can have an arbitrary number of transitions. Each transition contains a reference to a transition-trigger method and a target-state id.  (A transition trigger returns true if a state-change condition is fulfilled, false if not.)

 1 // set up state- machine diagram
 2 m_sm = new cStatemachine(NUMBER_OF_STATES);
 3
 4 m_sm.AddState(ST_ANCHORING, StateAnchoring);
 5 m_sm.AddTransition(ST_ANCHORING, IsAnchorUp, ST_FLOATING);
 6
 7 m_sm.AddState(ST_FLOATING, StateFloating);
 8 m_sm.AddTransition(ST_FLOATING, IsSailOut, ST_SAILING);
 9 m_sm.AddTransition(ST_FLOATING, IsAnchorDown, ST_ANCHORING);
10
11 m_sm.AddState(ST_SAILING, StateSailing);
12 m_sm.AddTransition(ST_FLOATING, IsSailIn, ST_FLOATING);
13
14 // set initial state
15 m_statemachine.SetState(STATE_ANCHORING);

Let’s have a closer look at the example (Look here for a more extensive version): There you see the state machine of the object “Boat”. It starts in the state “STATE_ANCHORING” (line 15). As soon as the method “IsAnchorUp()” returns true, the corresponding transition kicks in and it changes the state to “STATE_FLOATING” (line 5). If the sailors should decide to lower the anchor again, the second transition of the floating state will cause the boat to return into “STATE_ANCHORING” (line 9) again. You are now surely able to easily understand what the other states do and how they interact.

Besides all the good things this approach brings into your project, it also has its downside: It is simply a bit of more work. The programmer has to think about the design of the state machine constantly while programming. It only works if you design your states well. In addition, a little bit more code is needed to implement a particular functionality, too.

Still, the benefits you can gain from this methodology are well worth it. This especially applies to a distributed multi-programmer environment like ours where you need to understand other people’s code fast.
Happy coding! :-)

Popularity: 8% [?]

  • Share/Bookmark
Tags: , ,

Asserting the Basics III – Unit Testing

Posted in Methodology, Programming, Tips on January 8th, 2009 by Manuel
No Gravatar

Hello, welcome back and a happy new year to all readers from me as well!

Today, we are going to shed some light on our unit-testing process. It is one of our vital safety parts to ensure that our development process is still on track and that we are not breaking anything.

Before we started the project, I did some research on unit testing with AS3 on the web. I stumbled upon AsUnit at http://www.asunit.org by Luke Bayes and Ali Mills. I integrated their framework into our newly set-up project and we were able to start coding right away. Just now I realized that the  tutorial to set up AsUnit with FlashDevelop disappeared from the web. So, I decided to quickly assemble a new tutorial. You can find it at my web page http://www.ruelke.net.

When we started using it in real-life, we discovered a few minor issues. Consequently, I added and changed a couple of things over time, which transformed AsUnit into our very own version.

  1. Process: Run tests first: if one fails do not start project code
  2. Extension: Our Asserts can be checked
  3. Extension: Controlling the debug output

Ok, let’s explore these things a little further:

1. Run Tests First: If One Fails Do Not Start Project Code

Out of the box, AsUnit just doesn’t do anything after it has finished the tests. We all are regular game programmers that have used C++ before. So our unit-testing experience revolves around UnitTest++. I tried to get AsUnit to behave as similarly as possible. Accordingly, AsUnit needs to do the following:
- Start and Run the tests
- Check the result
- Continue with the game code if everything was correct

To do this, I extended the TestRunner-constructor with another parameter: a function pointer (callback). The callback is called within the TestRunner’s testCompleteHandler-function if the unit tests passed successfully. Then the callback takes care of continuing with the game — starting the game loop, loading assets and so on.

2. Our Asserts Can Be Checked

It is important to integrate our assert-handling into the testing process. You may remember my posts about assert-handling a while back (http://blog.rough-sea.com/category/methodology/). It is possible to use our debug class’ HasAsserts()-method, which returns true if an assertion failed, to check if an assertion actually fails as expected in a test case. The AsUnit framework has a class called Assert, which contains all checking methods used in the tests. By using what’s already there (like the assertTrue method), I could realize the check for assertions in a very simple way :

/**
 * Asserts assertions.
 * Usage: assertError(m_object.ErroneousMethod())
 * If assertion does not fail an AssertionFailedError is thrown
 * with the given message.
 */
static public function assertError(...args:Array):void
{
    assertTrue("Assertion failed to fail!", Debug.HasAsserts());
    // clear the asserts so there is
    // no interference with other tests
    Debug.ClearAsserts();
}

3. Controlling The Debug Output

A lot of our classes, especially the state machines, contain automated debug output. When running our tests, this debug output is overwhelming … and useless as well. All of the console output goes through our Debug class. So I added a flag that is able to disable the sending of messages to the console. This flag is activated right before the tests and deactivated right after. The spam was history.

Alrighty, these are the three most important changes I did to the AsUnit framework to make it suit our needs. Well, you have reached the end of my little trilogy. Don’t worry, I still have stuff to talk about.
So see you next time and happy coding! ;)

Popularity: 13% [?]

  • Share/Bookmark
Tags: , ,

Secure your Webserver (part 1)

Posted in Server Administration, Tips, Web security on January 5th, 2009 by Ole
No Gravatar

Hello ,

today I am going to write about server security. I decided to split the post into 3 parts. The future parts will be published during the next weeks.

Part 1 deals with server security in general and the conception of your personal strategy to avoid security problems.

Security has become more and more important in the last 10 years , as the numbers of internet users and server services are growing year by year. Especially the web 2.0 revolution brings new security problems. Nowadays users with a bad understanding of the technical background are setting up blogs, forums, websites and other services.

Therefore I wrote a small introduction for those newcomers!

In general there are 3 main areas you have to keep an eye on.

1. Network infrastructure:

I will not deal with this, because usually only professionals can influence the network infrastructure or your provider does this for you with routing , firewalls and filters.

2. Operating System:

A big part of security solutions and problems rely on the chosen operating system. All common operating systems (e.g. Linux, Windows, Unix) have advantages and disadvantages. If you expect me to write down which is the best one, I WILL NOT ! Nobody can tell you. It depends on so many factors like the services you want to run, your personal knownledge about the operating system, etc… . Maybe you do not even have the chance to choose your OS as your provider pre installed already one for you.

Unfortunately, I will have to focus on one operating system in the second part of my post. In my point of view the most common one is Linux. Although I am aware of the fact that other operating systems are great. So do not bug me with comments like: “You hate Windows ! Why not using FreeBSD ?  Solaris is the best one !” ;-)

3. Applications & Daemons

Daemons or services are the core of your security solutions and also the source of most security issues. Before offering several services, e.g. a Web-Server , Ftp-Server or an Email-Server, think about which services you really need and if it is really smart to offer all services on a single machine. Every application can be corrupted or compromised. Avoiding services and daemons is always a clever strategy to minimize the risks. Moreover security tools hinder security issues as well, for example virus scanner, firewall, spam filter, a handy user rights management, etc… .

Please think about all these facts before you run a public server.

The second part will be about the practical parts of server management to build a secure server. We are going to leave the boring theory, promised!

Popularity: 8% [?]

  • Share/Bookmark
Tags: , , ,