Add start of 'Intro to Clean Architectures' and 'Automatic Unit Testing' chapters

This commit is contained in:
rubenwardy 2018-07-10 23:26:26 +01:00
parent 5afc7b4d1e
commit 712b151de6
No known key found for this signature in database
GPG Key ID: A1E29D52FF81513C
4 changed files with 939 additions and 4 deletions

View File

@ -26,7 +26,7 @@
link: chapters/creating_textures.html
- title: Node Drawtypes
description: Guide to all drawtypes, including node boxes/nodeboxes and mesh nodes.
description: Guide to all drawtypes, including node boxes/nodeboxes and mesh nodes.
num: 5
link: chapters/node_drawtypes.html
@ -102,16 +102,28 @@
link: chapters/common_mistakes.html
- title: Automatic Error Checking
description: Use LuaCheck to find errors
description: Use LuaCheck as a linter to find errors
num: 20
link: chapters/luacheck.html
- title: Releasing a Mod
- title: Intro to Clean Architectures
description: Separate logic from data and API calls using the MVC pattern - Model View Controller.
num: 21
link: chapters/mvc.html
- title: Automatic Unit Testing
description: Write tests for your mods using Busted.
num: 22
link: chapters/unit_testing.html
- hr: true
- title: Releasing a Mod
num: 23
link: chapters/releasing.html
- title: More Resources
num: 22
num: 24
link: chapters/readmore.html
- hr: true

193
en/chapters/mvc.md Normal file
View File

@ -0,0 +1,193 @@
---
title: Intro to Clean Architectures
layout: default
root: ../../
---
## Introduction
Once your mod reaches a respectable size, you'll find it harder and harder to
keep the code clean and free of bugs. This is an especially big problem when using
a dynamically typed language like Lua, given that the compiler gives you very little
compiler-time help when it comes to things like making sure that types are used correctly.
This chapter covers important concepts needed to keep your code clean,
and common design patterns to achieve that. Please note that this chapter isn't
meant to be prescriptive, but to instead give you an idea of the possibilities.
There is no one good way of designing a mod, and good mod design is very subjective.
* [Cohesion, Coupling, and Separation of Concerns](#cohesion-coupling-and-separation-of-concerns)
* [Model-View-Controller](#model-view-controller)
* [API-View](#api-view)
## Cohesion, Coupling, and Separation of Concerns
Without any planning, a programming project will tend to gradually descend into
spaghetti code. Spaghetti code is characterised by a lack of structure - all the
code is thrown in together with no clear boundaries. This ultimately makes a
project completely unmaintainable, ending in its abandonment.
The opposite of this is to design your project as a collection of interacting
smaller programs or areas of code.
> Inside every large program, there is a small program trying to get out.
>
> --C.A.R. Hoare
This should be done in such a way that you achieve Separation of Concerns -
each area should be distinct and address a separate need or concern.
These programs/areas should have the following two properties:
* **High Cohesion** - the area should be closely/tightly related.
* **Low Coupling** - keep dependencies between areas as low as possible, and avoid
relying on internal implementations. It's a very good idea to make sure you have
a low amount of coupling, as this means that changing the APIs of certain areas
will be more feasible.
Note that these apply both when thinking about the relationship between mods,
and the relationship between areas inside a mod. In both cases you should try
to get high cohesion and low coupling.
## Model-View-Controller
In the next chapter we will discuss how to automatically test your code, and one
of the problems we will have is how to separate your logic
(calculations, what should be done) from API calls (`minetest.*`, other mods)
as much as possible.
One way to do this is to think about:
* What **data** you have.
* What **actions** you can take with this data.
* How **events** (ie: formspec, punches, etc) trigger these actions, and how
these actions cause things to happen in the engine.
Let's take an example of a land protection mod. The data you have is the areas
and any associated meta data. The actions you can take are `create`, `edit`, or
`delete`. The events that trigger these actions are chat commands and formspec
receive fields. These are 3 areas can usually be separated pretty well.
In your tests, you will be able to make sure that an action when triggered does the right thing
to the data, but you won't need to test that an event calls an action (as this
would require using the Minetest API, and this area of code should be made as
small as possible anyway.)
You should write your data representation using Pure Lua. "Pure" in this context
means that the functions could run outside of Minetest - none of the engine's
functions are called.
{% highlight lua %}
-- Data
function land.create(name, area_name)
land.lands[aname] = {
name = area_name,
owner = name,
-- more stuff
}
end
function land.get_by_name(area_name)
return land.lands[area_name]
end
{% endhighlight %}
Your actions should also be pure, however calling other functions is more
acceptable.
{% highlight lua %}
-- Controller
function land.handle_create_submit(name, area_name)
-- process stuff (ie: check for overlaps, check quotas, check permissions)
land.create(name, area_name)
end
function land.handle_creation_request(name)
-- This is a bad example, as explained later
land.show_create_formspec(name)
end
{% endhighlight %}
Your event handlers will have to interact with the Minetest API. You should keep
the amount of calculations to a minimum, as you won't be able to test this area
very easily.
{% highlight lua %}
-- View
function land.show_create_formspec(name)
-- Note how there's no complex calculations here!
return [[
size[4,3]
label[1,0;This is an example]
field[0,1;3,1;area_name;]
button_exit[0,2;1,1;exit;Exit]
]]
end
minetest.register_chatcommand("/land", {
privs = { land = true },
func = function(name)
land.handle_creation_request(name)
end,
})
minetest.register_on_player_receive_fields(function()
end)
{% endhighlight %}
The above is the Model-View-Controller pattern. The model is a collection of data
with minimal functions. The view is a collection of functions which listen to
events and pass it to the controller, and also receives calls from the controller to
do something with the Minetest API. The controller is where the decisions and
most of the calculations are made.
The controller should have no knowledge about the Minetest API - notice how
there are no Minetest calls or any view functions that resemble them.
You should *NOT* have a function like `view.hud_add(player, def)`.
Instead the view defines some actions the controller can tell the view to do,
like `view.add_hud(info)` where info is a value or table which doesn't relate
to the Minetest API at all.
<figure class="right_image">
<img
width="100%"
src="{{ page.root }}/static/mvc_diagram.svg"
alt="Diagram showing a centered text element">
</figure>
It is important that each area only communicates with its direct neighbours,
as shown above, in order to reduce how much you needs to change if you modify
an area's internals or externals. For example, to change the formspec you
would only need to edit the view. To change the view API, you would only need to
change the view and the controller, but not the model at all.
In practice, this design is rarely used because of the increased complexity
and because it doesn't give many benefits for most types of mods. Instead,
you tend to see a lot more of a less formal and strict kind of design -
varients of the API-View.
## API-View
In an ideal world, you'd have the above 3 areas perfectly separated with all
events going into the controller before going back to the normal view. But
this isn't the real world. A good half-way house is to reduce the mod into 2
parts:
* **API** - what was the model and controller. There should be no uses of
`minetest.` here.
* **View** - the view as before. It's a good idea to structure this into separate
files for each type of event.
rubenwardy's [crafting mod](https://github.com/rubenwardy/crafting) follows
this design. `api.lua` is almost all pure Lua functions handling the data
storage and controller-style calculations. `gui.lua` and `async_crafter.lua`
are views for each type of thing.
Separating the mod like this means that you can very easily test the API part,
as it doesn't use any Minetest APIs - as shown in the
[next chapter](unit_testing.html) and seen in the crafting mod.

View File

@ -0,0 +1,39 @@
---
title: Automatic Unit Testing
layout: default
root: ../../
---
## Introduction
Unit tests are an essential tool in proving and resuring yourself that your code
is correct. This chapter will show you how to write tests for Minetest mods and
games using busted, and how to structure your code to make this easier - Writing
unit tests for functions where you call Minetest functions is quite difficult,
but luckily [in the previous chapter](mvc.html), we discussed how to make your
code avoid this.
* [Installing Busted](#installing-busted)
* [Windows](#windows)
* [Linux](#linux)
## Installing Busted
### Windows
*Todo. No one cares about windows, right?*
### Linux
First you'll need to install LuaRocks:
sudo apt install luarocks
You can then install Busted globally:
sudo luarocks install busted
Check that it's installed with the following command:
busted --version

691
static/mvc_diagram.svg Normal file
View File

@ -0,0 +1,691 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="780"
height="300"
viewBox="0 0 206.37499 79.375003"
version="1.1"
id="svg8"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="unit_testing_mvc.svg">
<defs
id="defs2">
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0.0"
refX="0.0"
id="marker5762"
style="overflow:visible;"
inkscape:isstock="true">
<path
id="path5760"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(0.6) rotate(180) translate(0,0)" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible;"
id="marker5430"
refX="0.0"
refY="0.0"
orient="auto"
inkscape:stockid="Arrow2Mend"
inkscape:collect="always">
<path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
id="path5428" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0.0"
refX="0.0"
id="marker4938"
style="overflow:visible;"
inkscape:isstock="true">
<path
id="path4936"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#ffffff;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(0.6) rotate(180) translate(0,0)" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0.0"
refX="0.0"
id="marker4312"
style="overflow:visible;"
inkscape:isstock="true"
inkscape:collect="always">
<path
id="path4310"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#ffffff;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(0.6) rotate(180) translate(0,0)" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible;"
id="marker3786"
refX="0.0"
refY="0.0"
orient="auto"
inkscape:stockid="Arrow2Mend"
inkscape:collect="always">
<path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#ffffff;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
id="path3784" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible;"
id="marker2332"
refX="0.0"
refY="0.0"
orient="auto"
inkscape:stockid="Arrow2Mend">
<path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#ffffff;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
id="path2330" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible;"
id="marker2256"
refX="0.0"
refY="0.0"
orient="auto"
inkscape:stockid="Arrow2Mend">
<path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#ffffff;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
id="path2254" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0.0"
refX="0.0"
id="marker2136"
style="overflow:visible;"
inkscape:isstock="true">
<path
id="path2134"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#ffffff;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(0.6) rotate(180) translate(0,0)" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0.0"
refX="0.0"
id="marker2076"
style="overflow:visible;"
inkscape:isstock="true">
<path
id="path2074"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#ffffff;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(0.6) rotate(180) translate(0,0)" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible;"
id="marker1719"
refX="0.0"
refY="0.0"
orient="auto"
inkscape:stockid="Arrow2Mend"
inkscape:collect="always">
<path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#ffffff;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
id="path1717" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0.0"
refX="0.0"
id="Arrow2Mend"
style="overflow:visible;"
inkscape:isstock="true"
inkscape:collect="always">
<path
id="path982"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#ffffff;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(0.6) rotate(180) translate(0,0)" />
</marker>
<marker
inkscape:stockid="Arrow1Lend"
orient="auto"
refY="0.0"
refX="0.0"
id="Arrow1Lend"
style="overflow:visible;"
inkscape:isstock="true">
<path
id="path958"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
transform="scale(0.8) rotate(180) translate(12.5,0)" />
</marker>
<marker
inkscape:stockid="Arrow2Lstart"
orient="auto"
refY="0.0"
refX="0.0"
id="Arrow2Lstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path973"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(1.1) translate(1,0)" />
</marker>
<linearGradient
inkscape:collect="always"
id="linearGradient829">
<stop
style="stop-color:#8cb0ef;stop-opacity:1"
offset="0"
id="stop825" />
<stop
style="stop-color:#e6a58d;stop-opacity:1"
offset="1"
id="stop827" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient829"
id="linearGradient831"
x1="0"
y1="257.57774"
x2="206.33208"
y2="257.57774"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.75975991,0,0,0.13356955,0.78681326,192.78677)" />
<filter
id="filter3864"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur3866"
stdDeviation="0.20490381"
inkscape:collect="always" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.3450883"
inkscape:cx="406.45996"
inkscape:cy="138.00654"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-grids="true"
inkscape:snap-to-guides="true"
inkscape:snap-others="false"
inkscape:object-nodes="false"
inkscape:snap-nodes="false"
inkscape:snap-bbox="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1877"
inkscape:window-height="1080"
inkscape:window-x="43"
inkscape:window-y="0"
inkscape:window-maximized="1">
<sodipodi:guide
position="155.55086,49.31123"
orientation="0,1"
id="guide2463"
inkscape:locked="false" />
<sodipodi:guide
position="159.29263,8.0180861"
orientation="0,1"
id="guide2465"
inkscape:locked="false" />
<sodipodi:guide
position="168.51343,42.629491"
orientation="1,0"
id="guide2467"
inkscape:locked="false" />
<sodipodi:guide
position="163.77876,32.96439"
orientation="1,0"
id="guide5414"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-217.625)">
<rect
style="opacity:1;fill:url(#linearGradient831);fill-opacity:1;stroke:none;stroke-width:1.71643519;stroke-miterlimit:4;stroke-dasharray:none"
id="rect815"
width="156.76283"
height="10.674023"
x="0.78681326"
y="221.85422" />
<rect
y="235.07881"
x="1.3982887"
height="60.23225"
width="156.09898"
id="rect881"
style="opacity:1;fill:#800000;fill-opacity:1;stroke:#000000;stroke-width:0.69983035;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#3838c7;fill-opacity:1;stroke:#000000;stroke-width:0.52879667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect875"
width="101.43864"
height="52.919735"
x="3.450928"
y="240.0714" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.52916667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833"
width="40.642349"
height="40.642349"
x="7.418942"
y="248.13367" />
<rect
y="248.13367"
x="58.691807"
height="40.642349"
width="40.642349"
id="rect835"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.52916667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.52916667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect837"
width="40.642349"
height="40.642349"
x="109.96468"
y="248.13367" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:6.3499999px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="61.448162"
y="271.08517"
id="text5918"><tspan
sodipodi:role="line"
x="61.448162"
y="271.08517"
id="tspan5916"
style="stroke-width:0.26458332"><tspan
x="61.448162"
y="271.08517"
style="font-size:7.05555582px;stroke-width:0.26458332"
id="tspan5914">Controller</tspan></tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:6.3499999px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="17.132668"
y="271.08517"
id="text5912"><tspan
sodipodi:role="line"
x="17.132668"
y="271.08517"
id="tspan5910"
style="stroke-width:0.26458332"><tspan
x="17.132668"
y="271.08517"
style="font-size:7.05555582px;stroke-width:0.26458332"
id="tspan5908">Model</tspan></tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:6.3499999px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="122.02969"
y="271.08517"
id="text5906"><tspan
sodipodi:role="line"
x="122.02969"
y="271.08517"
id="tspan5904"
style="stroke-width:0.26458332"><tspan
x="122.02969"
y="271.08517"
style="font-size:7.05555582px;stroke-width:0.26458332"
id="tspan5902">View</tspan></tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:6.3499999px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="44.151394"
y="245.68427"
id="text879"><tspan
sodipodi:role="line"
id="tspan877"
x="44.151394"
y="245.68427"
style="font-size:4.93888903px;fill:#ffffff;fill-opacity:1;stroke-width:0.26458332">Testable</tspan></text>
<text
id="text885"
y="243.28423"
x="118.80692"
style="font-style:normal;font-weight:normal;font-size:6.3499999px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve"><tspan
style="font-size:4.93888903px;fill:#ffffff;fill-opacity:1;stroke-width:0.26458332"
y="243.28423"
x="118.80692"
id="tspan883"
sodipodi:role="line">Your Mod</tspan></text>
<rect
style="opacity:1;fill:#008000;fill-opacity:1;stroke:#000000;stroke-width:0.62945974;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect887"
width="47.333485"
height="60.269806"
x="157.67842"
y="235.04587" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.79999995;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)"
d="m 109.61309,263.60412 h -9.07142"
id="path953"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path1715"
d="m 99.56328,268.40006 h 9.07142"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.79999995;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1719)" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path2066"
d="m 58.469376,263.71751 h -9.07142"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.79999995;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker2256)" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path2126"
d="M 183.66188,263.60411 H 151.91875"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.79999995;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker2332)" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.79999995;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker2136)"
d="m 150.84016,268.40005 h 31.64313"
id="path2128"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<rect
style="opacity:1;fill:#69f869;fill-opacity:1;stroke:#000000;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect2431"
width="15.846315"
height="17.145483"
x="183.67453"
y="257.63821" />
<rect
y="243.22878"
x="164.0835"
height="17.145483"
width="15.846315"
id="rect839"
style="opacity:1;fill:#69f869;fill-opacity:1;stroke:#000000;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:6.3499999px;line-height:1.04999995;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="172.01414"
y="250.97433"
id="text5948"><tspan
sodipodi:role="line"
x="172.01414"
y="250.97433"
id="tspan5942"
style="stroke-width:0.26458332"><tspan
x="172.01414"
y="250.97433"
style="font-size:7.05555582px;line-height:1.04999995;text-align:center;text-anchor:middle;stroke-width:0.26458332"
id="tspan5938">Mod</tspan><tspan
dx="0"
x="186.89696"
y="250.97433"
style="font-size:7.05555582px;line-height:1.04999995;text-align:center;text-anchor:middle;stroke-width:0.26458332"
id="tspan5940" /></tspan><tspan
sodipodi:role="line"
x="177.20935"
y="258.38269"
id="tspan5946"
style="stroke-width:0.26458332"><tspan
x="177.20935"
y="258.38269"
style="font-size:7.05555582px;line-height:1.04999995;text-align:center;text-anchor:middle;stroke-width:0.26458332"
id="tspan5944">1</tspan></tspan></text>
<rect
y="271.8316"
x="164.05061"
height="17.145483"
width="15.846315"
id="rect2433"
style="opacity:1;fill:#69f869;fill-opacity:1;stroke:#000000;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:6.3499999px;line-height:1.04999995;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="171.98126"
y="279.38046"
id="text5960"><tspan
sodipodi:role="line"
x="171.98126"
y="279.38046"
id="tspan5954"
style="stroke-width:0.26458332"><tspan
x="171.98126"
y="279.38046"
style="font-size:7.05555582px;line-height:1.04999995;text-align:center;text-anchor:middle;stroke-width:0.26458332"
id="tspan5950">Mod</tspan><tspan
dx="0"
x="186.86407"
y="279.38046"
style="font-size:7.05555582px;line-height:1.04999995;text-align:center;text-anchor:middle;stroke-width:0.26458332"
id="tspan5952" /></tspan><tspan
sodipodi:role="line"
x="177.17647"
y="286.78879"
id="tspan5958"
style="stroke-width:0.26458332"><tspan
x="177.17647"
y="286.78879"
style="font-size:7.05555582px;line-height:1.04999995;text-align:center;text-anchor:middle;stroke-width:0.26458332"
id="tspan5956">2</tspan></tspan></text>
<g
inkscape:label="Layer 1"
id="layer1-6"
transform="matrix(0.26458333,0,0,0.26458333,185.12015,259.57696)">
<path
sodipodi:nodetypes="ccccccc"
transform="translate(3.4641013,6)"
id="path3047"
d="M 6.1513775e-7,16 3.2110204e-7,28 21.035899,40.145082 l 21,-12.414519 V 16.269437 L 20.78461,4 Z"
style="fill:#e9b96e;fill-opacity:1;stroke:#573a0d;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="ccccccccccc"
id="path3831"
d="m 8.5,30.907477 -2,-1.1547 v 6 L 17.320508,42 V 40 L 15.588457,39 V 37 L 13.5,35.794229 v -4 l -5,-2.886752 z"
style="fill:#2e3436;fill-opacity:1;stroke:#2e3436;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="cccccccc"
id="path3870"
d="M 6.9282032,36 10.392305,34 13.856406,36 15.5,36.948929 v 2 l 2,1.154701 v 2 z"
style="opacity:1;fill:#555753;fill-opacity:1;stroke:#2e3436;stroke-linejoin:miter"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="cccccccccc"
id="path3851"
d="M 25.980762,19 31.5,22.186533 v 2 L 38.09375,28 41.5625,26 45.5,23.730563 v 2.538874 -4 L 32.908965,15 Z"
style="fill:#fce94f;fill-opacity:1;stroke:#625802;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="ccccc"
id="path5684"
d="m 24.839746,18.341234 8.660254,-5 v 2 l -8.660254,5 z"
style="fill:#e9b96e;fill-opacity:1;stroke:#573a0d;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.5;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
transform="translate(0,4)"
sodipodi:nodetypes="ccccccc"
id="path3821"
d="M 25.980762,5 3.4641016,18 17.5,26.10363 l 14,-7.917097 -6.660254,-3.845299 8.660254,-5 z"
style="fill:#73d216;fill-opacity:1;stroke:#325b09;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
transform="translate(0,4)"
sodipodi:nodetypes="ccccccccccc"
id="path3825"
d="m 17.5,28.10363 v 2 L 19.052559,31 v 2 L 24.5,36.145082 l 12,-7.071797 v -2.14657 l 2,-1.1547 v -1.54403 l -7,-4.041452 z"
style="fill:#729fcf;fill-opacity:1;stroke:#19314b;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
<g
style="stroke-linejoin:miter"
id="g5691">
<path
style="opacity:0.25;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter3864)"
d="m 13.856406,20 6.928204,4 -6.928204,4 -6.9282028,-4 z"
id="path3862"
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0" />
<g
style="stroke-linejoin:miter"
id="g3858">
<path
transform="translate(-3.4641015,2)"
sodipodi:nodetypes="ccccccc"
id="path3833"
d="m 15.588457,21 1.732051,1 1.732051,-1 v -6 l -1.732051,-1 -1.732051,1 z"
style="fill:#c17d11;fill-opacity:1;stroke:#8f5902;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="ccccccc"
transform="translate(-3.4641015,2)"
id="path3837"
d="M 9.9641015,13.752777 17.320508,18 23.964101,14.164319 V 5.8356805 L 17.320508,2 9.9641015,6.2472233 Z"
style="fill:#4e9a06;fill-opacity:1;stroke:#316004;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
</g>
<g
style="stroke-linejoin:miter"
transform="translate(-4.2591582e-7,2)"
id="g5686">
<path
sodipodi:nodetypes="ccccc"
id="path3868"
d="m 13.856406,20 5.196153,3 -5.196153,3 -5.196152,-3 z"
style="opacity:0.25;fill:#2e3436;fill-opacity:1;stroke:none;stroke-linejoin:miter;filter:url(#filter3864)"
transform="translate(24.248712,-2)"
inkscape:connector-curvature="0" />
<path
transform="translate(20.78461)"
sodipodi:nodetypes="ccccccc"
id="path3853"
d="M 15.71539,21.073285 17.320508,22 18.71539,21.194664 V 12.805336 L 17.320508,12 15.71539,13.073285 Z"
style="fill:#4e9a06;fill-opacity:1;stroke:#316004;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
<path
sodipodi:nodetypes="cc"
id="path3872"
d="M 12.124356,33 11.25833,32.5"
style="fill:none;fill-opacity:1;stroke:#ef2929;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.5, 0.5;stroke-dashoffset:0.25;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="ccccccccc"
id="path3874"
d="m 45.5,26.730563 -4,2.309401 v 1 l -2,1.1547 v 2 l -2,1.154701 v 4 l 8,-4.618802 z"
style="fill:#888a85;stroke:#2e3436;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.5;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path3780"
d="M 163.83693,250.30584 H 151.9379"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.79999995;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker3786)" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.79999995;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker4312)"
d="M 150.83551,255.10178 H 162.8083"
id="path3782"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.79999995;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker4938)"
d="M 163.76738,277.79133 H 151.86835"
id="path4932"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path4934"
d="m 150.76596,282.58727 h 11.97279"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.79999995;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker5430)" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:6.3499999px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="3.4935899"
y="229.46086"
id="text5746"><tspan
sodipodi:role="line"
id="tspan5744"
x="3.4935899"
y="229.46086"
style="stroke-width:0.26458332">Pure</tspan></text>
<text
id="text5750"
y="228.94305"
x="139.43495"
style="font-style:normal;font-weight:normal;font-size:6.3499999px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve"><tspan
style="stroke-width:0.26458332"
y="228.94305"
x="139.43495"
id="tspan5748"
sodipodi:role="line">Dirty</tspan></text>
<path
style="fill:#000000;stroke:#000000;stroke-width:0.26458333;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;marker-end:url(#Arrow1Lend);marker-start:url(#Arrow2Lstart)"
d="M 22.030771,227.56373 H 133.95495"
id="path5752"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB