104 Commits

Author SHA1 Message Date
Sven Balzer 2d576499df do not show parade for ranged combat talents 2025-05-29 15:53:23 +02:00
Sven Balzer ac03b7e758 fix calculation of ranged combat talents attack value 2025-05-29 15:48:35 +02:00
Sven Balzer f21a5ebf06 allow spell rolls to be affeted by MR 2025-05-24 16:20:21 +02:00
Sven Balzer ae0ca2018f add simple zauber rolls 2025-05-24 15:55:41 +02:00
Sven Balzer 1cb0f89dff add text shadow to die values 2025-05-24 15:02:09 +02:00
Sven Balzer d6983cf9c7 move zauber into their own tab 2025-05-24 14:59:13 +02:00
Sven Balzer af3de8f98b rename actor sheet tabs 2025-05-24 12:55:19 +02:00
Sven Balzer d281756053 move actorsheet nav tabs to the right of the application 2025-05-24 07:50:53 +02:00
Sven Balzer 9a37858ea5 simplify nav tabs in actor sheet 2025-05-23 12:47:09 +02:00
Sven Balzer f6e150eb10 add LaengeField and use it for Bewaffnung and Character 2025-05-23 10:57:22 +02:00
Sven Balzer aaa01834a8 add very basic implementation of Zauber that just posts its beschreibung to chat 2025-05-16 08:18:30 +02:00
Sven Balzer 96851a7100 add a description field to all item types 2025-05-16 07:33:34 +02:00
Sven Balzer 3f18c11456 add allgemein tab for characters 2025-05-12 21:50:32 +02:00
Sven Balzer 78acc9def0 keep aspect ratio of images 2025-05-12 20:14:57 +02:00
Sven Balzer a1dc061fd8 use plural for default weight units 2025-05-06 22:10:27 +02:00
Sven Balzer 5c598ee037 fix weight display in inventory 2025-05-06 22:07:36 +02:00
Sven Balzer 1080597a24 use GewichtField for Bewaffnung 2025-05-06 19:27:12 +02:00
Sven Balzer cea81ea542 use GewichtField for Ruestung 2025-05-06 19:15:49 +02:00
Sven Balzer 67bd01f56d add GewichtField and use it for Gegenstand 2025-05-06 16:05:50 +02:00
Sven Balzer 0639ce11d7 allow floating point Preis 2025-05-06 15:48:54 +02:00
Sven Balzer d72c4a81bd use PreisField for Bewaffnung 2025-05-06 15:43:13 +02:00
Sven Balzer ec0cda31aa use PreisField for Ruestung 2025-05-06 14:26:26 +02:00
Sven Balzer 32331729cc add PreisField and use it in Gegenstand 2025-05-06 13:33:13 +02:00
Sven Balzer 5e94198077 get rid of all usages of class="placeholder" outside of editable-input 2025-05-06 11:40:31 +02:00
Sven Balzer 975934f06a fix initiative calculation to use the rounded total BE 2025-05-06 11:31:23 +02:00
Sven Balzer b58d0c9ec7 fix parade crit calculation 2025-05-05 23:13:11 +02:00
Sven Balzer 9d4e8f7c9e replace almost all uses of editable-input with DSA41_input 2025-05-05 22:35:30 +02:00
Sven Balzer b54869ac5a fix nested fields field_name 2025-05-05 12:52:23 +02:00
Sven Balzer 942c6b2ce1 allow Handlebars string objects to be passed in as field_name 2025-05-05 12:51:48 +02:00
Sven Balzer f2c4b63382 add the ability to define a subtitle to DSA41_input 2025-05-05 11:58:50 +02:00
Sven Balzer f82548fbaf change VorNachteil.system.kategorie to be choices based and use DSA41_input 2025-05-04 12:16:10 +02:00
Sven Balzer 0cf675f58a change Sonderfertigkeit.system.kategorie to be choices based and use DSA41_input 2025-05-04 12:13:01 +02:00
Sven Balzer 6eb2b88046 change Bewaffnung.system.schild.groesse to be choices based and use DSA41_input 2025-05-04 12:08:27 +02:00
Sven Balzer e7f6e91516 change Kampftalent.system.kategorie to be choices based and use DSA41_input 2025-05-04 11:35:56 +02:00
Sven Balzer f296b2280d add SteigerungsKategorieField and use it for Kampftalent 2025-05-04 11:31:35 +02:00
Sven Balzer 0753dd44ab add options and context to AttributeChoiceField 2025-05-04 11:26:49 +02:00
Sven Balzer f6518cba74 change Talent.system.kategorie to be choices based and use DSA41_input 2025-05-04 11:15:31 +02:00
Sven Balzer a6dc61a924 add AttributeChoiceField and use it for Talent 2025-05-04 11:04:01 +02:00
Sven Balzer bc8c7f6a2b add DSA41_input Handlebars helper 2025-05-04 10:49:56 +02:00
Sven Balzer 5eeb4c7f57 fix localization fallback 2025-05-03 17:58:04 +02:00
Sven Balzer 3ae8533bf9 change styling of tabs and input to match the way it looked before v13 2025-05-03 12:01:38 +02:00
Sven Balzer 7036efd8fd add the ability to change the icon of actors and items 2025-05-03 11:26:06 +02:00
Sven Balzer 8922bb1b72 add the ability to reorder items in Inventory 2025-05-02 10:55:28 +02:00
Sven Balzer 4bc8645fcb use v13s default drop handling for actor sheets 2025-05-02 04:38:34 +02:00
Sven Balzer e340a68cff change minimum and verified foundry versions to 13 2025-05-01 10:14:18 +02:00
Sven Balzer b82f6ab305 guard against not having flags on the chat message 2025-05-01 10:12:59 +02:00
Sven Balzer d233593a9b fix css for chat targets 2025-05-01 10:06:56 +02:00
Sven Balzer c7e748e382 replace getHTML with renderHTML for ChatMessage 2025-05-01 09:55:19 +02:00
Sven Balzer 21ac6bf9fa put all flags inside dsa41 scope 2025-05-01 09:03:38 +02:00
Sven Balzer 6497042f97 get rid of old {Actor,Item}Sheet unregisters 2025-05-01 07:25:36 +02:00
Sven Balzer 735650358a reference loadTemplates and renderTemplate from their new namespace 2025-05-01 07:19:22 +02:00
Sven Balzer d653d58741 reference DocumentSheetConfig from the new namespace 2025-05-01 07:16:16 +02:00
Sven Balzer 51235ebaf2 reference Notifications from the new namespace 2025-05-01 07:06:50 +02:00
Sven Balzer 61bc68dd3c fix i18n fallback for v13 2025-05-01 07:01:39 +02:00
Ammerhai 76674f410e Update README.md 2025-04-19 15:51:13 +02:00
Sven Balzer 2933dcd3ea substract BE from INI 2025-03-02 12:28:02 +01:00
Sven Balzer 91944f97ba add icons to Bewaffnung 2025-03-01 18:05:55 +01:00
Sven Balzer f311a0c8e9 fix initiative 2025-03-01 16:08:05 +01:00
Sven Balzer 99f8e5929b add Rüstungen und bewaffnungen compendiums 2025-03-01 01:09:33 +01:00
Sven Balzer 151e6d3e13 give every data field an initial value and update zig structs 2025-02-15 14:04:38 +01:00
Sven Balzer abb0405035 make player tokens linked by default 2025-02-12 00:03:59 +01:00
Sven Balzer 66e7e93bae fix Talentprobe for negative TaW 2025-02-11 23:07:01 +01:00
Sven Balzer 474b2b6df7 allow (un)equipping items 2025-02-11 12:20:29 +01:00
Sven Balzer 437a27ad28 calculate and use eBE for Talent 2025-02-11 08:26:59 +01:00
Sven Balzer 7ac9649768 fix spelling of Säbel in compendium 2025-02-11 07:39:34 +01:00
Sven Balzer b1da5ff6f2 add Abenteuerpunkte to Eigenschaften tab 2025-02-04 23:37:16 +01:00
Sven Balzer 53bff7a4a6 add dialog to select Trefferzone on damage application 2025-02-04 22:59:44 +01:00
Sven Balzer add059a902 add currency display in inventory 2025-02-04 22:30:30 +01:00
Sven Balzer a3b6271bf3 add a damage application button to Trefferpunkte chat messages 2025-02-03 03:53:17 +01:00
Sven Balzer 43459113d5 fix kategorie for newly created items 2025-01-31 05:28:26 +01:00
Sven Balzer 04093d752e disable annoying screen resoultion too small warning 2025-01-31 05:26:22 +01:00
Sven Balzer ef00bd029e add more context to chat messages 2025-01-31 05:24:47 +01:00
Sven Balzer d7779f3c48 make empty lists show Keine 2024-12-22 16:47:07 +01:00
Sven Balzer b83213fc0b add Karmalenergie 2024-12-22 16:46:53 +01:00
Sven Balzer dcbb9f6c9c fix AT and PA calculation for weapons 2024-12-20 17:59:04 +01:00
Sven Balzer ef4539ee39 change Eigenschaften to be in a list instead of a fieldset 2024-12-20 17:44:33 +01:00
Sven Balzer fea5de6596 add bars for Ausdauer and Astralenergie, make basiswerte have modifiers 2024-12-20 14:14:07 +01:00
Sven Balzer 4079a40d6b change Kampftalente to be Items 2024-12-19 10:28:29 +01:00
Sven Balzer 57eb581ae7 add lebenspunkte 2024-12-18 07:55:27 +01:00
Sven Balzer b011a65510 fix BE of some talents 2024-12-18 07:48:43 +01:00
Sven Balzer 3dff7555e5 add VorNachteil 2024-12-05 23:56:22 +01:00
Sven Balzer 5ef47a483f add Sozialstatus 2024-12-05 22:50:42 +01:00
Sven Balzer f824286243 add Sonderfertigkeiten 2024-12-05 22:20:20 +01:00
Sven Balzer 942c395f59 add zig build for compendium packs
move document types from template.json into system.json
change Talents into Items
add rolls for Talents
change the fallback language to german
2024-11-23 18:29:11 +01:00
Sven Balzer 98dcb0749d add dialog to attributes for custom modifier 2024-11-12 01:04:03 +01:00
Sven Balzer 3946a8116c add dialog to attacke for custom modifier 2024-11-12 00:45:58 +01:00
Sven Balzer 7384e6cdcf add parade dialog for parrying crits 2024-11-12 00:25:47 +01:00
Sven Balzer 5179ecba91 add crits to nahkampfwaffe damage 2024-11-12 00:05:53 +01:00
Sven Balzer d9e1721459 add crits to fernkampf damage 2024-11-11 23:47:41 +01:00
Sven Balzer 998951bafc add fernkampf attack and damage rolls 2024-11-11 23:24:32 +01:00
Sven Balzer 6aa65be7a0 update ActorSheet to ActorSheetV2 and do a general cleanup pass
move editable-input partial into its own file so it doesn't have to be copy-pasted everywhere
change css to use nesting
2024-11-05 13:07:17 +01:00
Sven Balzer 2f9410180c update ItemSheets to ItemSheetV2 2024-11-01 20:21:09 +01:00
Sven Balzer 307307d271 add tooltips for attributes and combat rolls 2024-10-30 21:34:36 +01:00
Sven Balzer a6ae10beba add compatibility information to system.json 2024-10-28 13:06:36 +01:00
Sven Balzer 19297c4bd7 change file ending of handlebars templates to "hbs" from "html" for syntax highlighting 2024-10-27 18:23:03 +01:00
Sven Balzer 6e58e06058 compute Ruestungen stats 2024-10-19 00:42:15 +02:00
Sven Balzer 172e98f663 change d20 display 2024-10-18 21:12:17 +02:00
Sven Balzer 00fb647f0f add basic attribute and combat rolls 2024-10-17 19:59:31 +02:00
Sven Balzer e2ffb67d35 add Inventar and combat system base values 2024-10-12 16:58:13 +02:00
Sven Balzer 948dba6032 change Nahkampfwaffe to Bewaffnung and add Parierwaffe/Schild/Fernkampfwaffe 2024-10-08 14:28:46 +02:00
Sven Balzer 0df8a4e89d add Nahkampfwaffe 2024-10-05 22:44:10 +02:00
Sven Balzer 48e0c5db3c add Ruestungen 2024-10-05 13:36:35 +02:00
Sven Balzer d099e32fcc add Gegenstaende 2024-10-05 00:02:23 +02:00
Sven Balzer 442cae2598 add talents 2024-10-04 22:30:12 +02:00
205 changed files with 36876 additions and 264 deletions
+1 -2
View File
@@ -1,4 +1,3 @@
# dsa-4th-edition
https://gitlab.com/foundry-vtt-dsa/dsa-4.1-core/dsa-4.1-system
-> comparison for mechanics
Das Schwarze Auge 4.1/The Dark Eye 4.1
+22
View File
@@ -0,0 +1,22 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const compendiums_step = b.step("compendiums", "Build compendiums");
const package_step = b.step("package", "Build system zip");
const leveldb = b.dependency("leveldb", .{}).module("leveldb");
const compendium_creator = b.addExecutable(.{
.name = "compendium_creator",
.root_source_file = b.path("zig/compendium_creator.zig"),
.target = b.host,
.optimize = .Debug,
});
compendium_creator.root_module.addImport("leveldb", leveldb);
const build_compendiums = b.addRunArtifact(compendium_creator);
compendiums_step.dependOn(&build_compendiums.step);
const build_package = b.addSystemCommand(&.{ "7z", "-bsp0", "-bso0", "a", "dsa-4th-edition.zip", "src", "packs", "system.json" });
package_step.dependOn(compendiums_step);
package_step.dependOn(&build_package.step);
}
+14
View File
@@ -0,0 +1,14 @@
.{
.name = "dsa-4th-edition",
.version = "0.0.0",
.paths = .{
"build.zig",
"build.zig.zon",
"zig",
},
.dependencies = .{
.leveldb = .{
.path = "zig/libs/leveldb"
},
},
}
+605
View File
@@ -0,0 +1,605 @@
{{#*inline "die-type"}}
<div class="die die-{{type}} die-type">
<div><svg viewbox="0 0 64 64"><use href="/systems/dsa-4th-edition/src/Assets/d20.svg#d20"></use></svg></div>
<div>{{localize (concat "DSA41.attributes.short." type)}}</div>
</div>
{{/inline}}
{{#*inline "die-value"}}
<div class="col noflex center">
{{#if header}}<div class="center">{{header}}</div>{{/if}}
<div class="die die-{{type}}" data-action="roll" data-roll-type="{{type}}" {{#if success-value}}data-success-value="{{success-value}}"{{/if}} {{#if data-roll}}data-roll="{{data-roll}}"{{/if}} {{#if data-tooltip}}data-tooltip='{{> (lookup . "data-tooltip")}}'{{/if}}>
<div><svg viewbox="0 0 64 64"><use href="/systems/dsa-4th-edition/src/Assets/d20.svg#d20"></use></svg></div>
<div>{{{value}}}</div>
</div>
</div>
{{/inline}}
<div>
<nav class="tabs">
<a class="fas fa-feather {{#if (eq tabGroups.primary 'allgemein') }}active{{/if}}" data-action="tab" data-group="primary" data-tab="allgemein" data-tooltip-direction="RIGHT" data-tooltip="{{localize "DSA41.character.allgemein"}}"></a>
<a class="fas fa-cog {{#if (eq tabGroups.primary 'eigenschaften')}}active{{/if}}" data-action="tab" data-group="primary" data-tab="eigenschaften" data-tooltip-direction="RIGHT" data-tooltip="{{localize "DSA41.character.eigenschaften"}}"></a>
<a class="fas fa-list {{#if (eq tabGroups.primary 'talente') }}active{{/if}}" data-action="tab" data-group="primary" data-tab="talente" data-tooltip-direction="RIGHT" data-tooltip="{{localize "DSA41.character.talente"}}"></a>
<a class="fas fa-sack {{#if (eq tabGroups.primary 'inventar') }}active{{/if}}" data-action="tab" data-group="primary" data-tab="inventar" data-tooltip-direction="RIGHT" data-tooltip="{{localize "DSA41.character.inventar"}}"></a>
<a class="fas fa-swords {{#if (eq tabGroups.primary 'kampf') }}active{{/if}}" data-action="tab" data-group="primary" data-tab="kampf" data-tooltip-direction="RIGHT" data-tooltip="{{localize "DSA41.character.kampf"}}"></a>
<a class="fas fa-bolt {{#if (eq tabGroups.primary 'zauber') }}active{{/if}}" data-action="tab" data-group="primary" data-tab="zauber" data-tooltip-direction="RIGHT" data-tooltip="{{localize "DSA41.character.zauber"}}"></a>
</nav>
<div class="scroll-container">
<div class="actor-sheet ActorSheet" data-tooltip-class="DSA41">
<div class="grid5 gap">
{{DSA41_input "name" subtitle="DSA41.name"}}
{{DSA41_input "system.race" subtitle="DSA41.race"}}
{{DSA41_input "system.culture" subtitle="DSA41.culture"}}
{{DSA41_input "system.profession" subtitle="DSA41.profession"}}
{{DSA41_input "system.sozialstatus" subtitle="DSA41.sozialstatus"}}
</div>
<div class="row">
<img class="character-image" src="{{ actor.img }}" title="{{ actor.name }}" data-action="editImage" data-edit="img">
<div class="col">
<div class="row">
{{#each actor.system.attributes}}
{{>die-value type=@key header=(localize (concat "DSA41.attributes.short." @key)) value=(lookup @root.actor.system.computed.attributes @key) success-value=(lookup @root.actor.system.computed.attributes @key) data-roll="1d20" data-tooltip="attribute_tooltip"}}
{{/each}}
</div>
<div class="grid3">
<div class="bar hp" style="--bar-percentage: {{actor.system.computed.lebenspunkte.prozent}}%;">
{{DSA41_input "system.lebenspunkte.aktuell"}} / <span>{{actor.system.computed.lebenspunkte.max}}</span>
</div>
<div class="bar ausdauer" style="--bar-percentage: {{actor.system.computed.ausdauer.prozent}}%;">
{{DSA41_input "system.ausdauer.aktuell"}} / <span>{{actor.system.computed.ausdauer.max}}</span>
</div>
<div class="bar astralenergie" style="--bar-percentage: {{actor.system.computed.astralenergie.prozent}}%;">
{{DSA41_input "system.astralenergie.aktuell"}} / <span>{{actor.system.computed.astralenergie.max}}</span>
</div>
</div>
</div>
</div>
<div class="tab {{#if (eq tabGroups.primary 'allgemein')}}active{{/if}}" data-group="primary" data-tab="allgemein">
<div class="grid4 gap align-center">
<span>{{localize "DSA41.allgemein.geschlecht"}}</span>
{{DSA41_input "system.allgemein.geschlecht"}}
<span>{{localize "DSA41.allgemein.alter"}}</span>
{{DSA41_input "system.allgemein.alter"}}
<span>{{localize "DSA41.allgemein.groesse"}}</span>
{{DSA41_input "system.allgemein.groesse"}}
<span>{{localize "DSA41.allgemein.gewicht"}}</span>
{{DSA41_input "system.allgemein.gewicht"}}
<span>{{localize "DSA41.allgemein.haarfarbe"}}</span>
{{DSA41_input "system.allgemein.haarfarbe"}}
<span>{{localize "DSA41.allgemein.augenfarbe"}}</span>
{{DSA41_input "system.allgemein.augenfarbe"}}
<span>{{localize "DSA41.allgemein.stand"}}</span>
{{DSA41_input "system.allgemein.stand"}}
<span>{{localize "DSA41.allgemein.titel"}}</span>
{{DSA41_input "system.allgemein.titel"}}
</div>
<div class="grid2 gap">
<div>
<div class="center">{{localize "DSA41.allgemein.aussehen"}}</div>
{{DSA41_input "system.allgemein.aussehen" elementType="prose-mirror"}}
</div>
<div>
<div class="center">{{localize "DSA41.allgemein.hintergrund"}}</div>
{{DSA41_input "system.allgemein.hintergrund" elementType="prose-mirror"}}
</div>
</div>
<div>
</div>
<div>
<div class="center">{{localize "DSA41.allgemein.biografie"}}</div>
{{DSA41_input "system.allgemein.biografie" elementType="prose-mirror"}}
</div>
</div>
<div class="tab {{#if (eq tabGroups.primary 'eigenschaften')}}active{{/if}}" data-group="primary" data-tab="eigenschaften">
<div class="Abenteuerpunkte">
<span>{{localize "DSA41.abenteuerpunkte"}}:</span>
{{DSA41_input "system.abenteuerpunkte.ausgegeben"}} / {{DSA41_input "system.abenteuerpunkte.gesamt"}}
<span>({{actor.system.computed.abenteuerpunkte.uebrig}})</span>
</div>
<div class="list Eigenschaften">
<div class="list-header">
<span></span>
<span>{{localize "DSA41.attributes.initial"}}</span>
<span>{{localize "DSA41.attributes.advancement"}}</span>
<span>{{localize "DSA41.attributes.modifier"}}</span>
</div>
{{#each actor.system.attributes}}
<div class="list-item">
<span>{{localize (concat "DSA41.attributes.long." @key)}}</span>
{{DSA41_input (concat "system.attributes." @key ".initial")}}
{{DSA41_input (concat "system.attributes." @key ".advancement")}}
{{DSA41_input (concat "system.attributes." @key ".modifier")}}
</div>
{{/each}}
</div>
<div class="list Basiswerte">
<div class="list-header">
<span></span>
<span>{{localize "DSA41.basiswerte.label_basiswert"}}</span>
<span>{{localize "DSA41.basiswerte.label_modifikator"}}</span>
<span>{{localize "DSA41.basiswerte.label_zukauf"}}</span>
<span>{{localize "DSA41.basiswerte.label_verlust"}}</span>
<span>{{localize "DSA41.basiswerte.label_total"}}</span>
</div>
<div class="list-item">
<span>{{localize "DSA41.basiswerte.lebenspunkte"}}</span>
<span>{{actor.system.computed.lebenspunkte.basiswert}}</span>
<span>{{DSA41_input "system.lebenspunkte.modifikator"}}</span>
<span>{{DSA41_input "system.lebenspunkte.zukauf"}}</span>
<span>{{DSA41_input "system.lebenspunkte.verlust"}}</span>
<span>{{actor.system.computed.lebenspunkte.max}}</span>
</div>
<div class="list-item">
<span>{{localize "DSA41.basiswerte.ausdauer"}}</span>
<span>{{actor.system.computed.ausdauer.basiswert}}</span>
<span>{{DSA41_input "system.ausdauer.modifikator"}}</span>
<span>{{DSA41_input "system.ausdauer.zukauf"}}</span>
<span>{{DSA41_input "system.ausdauer.verlust"}}</span>
<span>{{actor.system.computed.ausdauer.max}}</span>
</div>
<div class="list-item">
<span>{{localize "DSA41.basiswerte.astralenergie"}}</span>
<span>{{actor.system.computed.astralenergie.basiswert}}</span>
<span>{{DSA41_input "system.astralenergie.modifikator"}}</span>
<span>{{DSA41_input "system.astralenergie.zukauf"}}</span>
<span>{{DSA41_input "system.astralenergie.verlust"}}</span>
<span>{{actor.system.computed.astralenergie.max}}</span>
</div>
<div class="list-item">
<span>{{localize "DSA41.basiswerte.karmalenergie"}}</span>
<span>{{DSA41_input "system.karmalenergie"}}</span>
<span></span>
<span></span>
<span></span>
<span>{{actor.system.karmalenergie}}</span>
</div>
<div class="list-item">
<span>{{localize "DSA41.basiswerte.magieresistenz"}}</span>
<span>{{actor.system.computed.magieresistenz.basiswert}}</span>
<span>{{DSA41_input "system.magieresistenz.modifikator"}}</span>
<span>{{DSA41_input "system.magieresistenz.zukauf"}}</span>
<span></span>
<span>{{actor.system.computed.magieresistenz.max}}</span>
</div>
<div class="list-item">
<span>{{localize "DSA41.basiswerte.initiative"}}</span>
<span>{{actor.system.computed.initiative.basiswert}}</span>
<span>{{DSA41_input "system.modifikator_initiative"}}</span>
<span></span>
<span></span>
<span>{{actor.system.computed.initiative.wert}}</span>
</div>
<div class="list-item">
<span>{{localize "DSA41.basiswerte.attacke"}}</span>
<span>{{actor.system.computed.attacke.basiswert}}</span>
<span>{{DSA41_input "system.modifikator_attacke"}}</span>
<span></span>
<span></span>
<span>{{actor.system.computed.attacke.wert}}</span>
</div>
<div class="list-item">
<span>{{localize "DSA41.basiswerte.parade"}}</span>
<span>{{actor.system.computed.parade.basiswert}}</span>
<span>{{DSA41_input "system.modifikator_parade"}}</span>
<span></span>
<span></span>
<span>{{actor.system.computed.parade.wert}}</span>
</div>
<div class="list-item">
<span>{{localize "DSA41.basiswerte.fernkampf"}}</span>
<span>{{actor.system.computed.fernkampf.basiswert}}</span>
<span>{{DSA41_input "system.modifikator_fernkampf"}}</span>
<span></span>
<span></span>
<span>{{actor.system.computed.fernkampf.wert}}</span>
</div>
</div>
<div class="grid2 gap">
<div class="list Vorteile">
<div class="list-header">
<div>{{localize (concat "DSA41.vornachteil.label_vorteile")}}</div>
</div>
{{#unless (ne actor.system.computed.num_vorteile 0)}}
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
{{/unless}}
{{#each actor.itemTypes.VorNachteil}}
{{#if (eq system.kategorie "vorteil")}}
<div class="list-item" data-item-id="{{_id}}">
<div>
<div class="fit-content" data-action="item-open" data-tooltip="<h4>{{name}}</h4>{{system.beschreibung}}">{{maybeLocalize name prefix=(concat "DSA41.vornachteil." system.kategorie ".name")}}</div>
</div>
<div></div>
<div class="center fas fa-trash" data-action="item-delete"></div>
</div>
{{/if}}
{{/each}}
</div>
<div class="list Nachteile subgrid-rows">
<div class="list-header">
<div>{{localize (concat "DSA41.vornachteil.label_nachteile")}}</div>
</div>
{{#unless (ne actor.system.computed.num_nachteile 0)}}
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
{{/unless}}
{{#each actor.itemTypes.VorNachteil}}
{{#if (eq system.kategorie "nachteil")}}
<div class="list-item" data-item-id="{{_id}}">
<div>
<div class="fit-content" data-action="item-open" data-tooltip="<h4>{{name}}</h4>{{system.beschreibung}}">{{maybeLocalize name prefix=(concat "DSA41.vornachteil." system.kategorie ".name")}}</div>
</div>
<div></div>
<div class="center fas fa-trash" data-action="item-delete"></div>
</div>
{{/if}}
{{/each}}
</div>
</div>
<div class="list Sonderfertigkeiten">
<div class="list-header">
<div>{{localize (concat "DSA41.sonderfertigkeiten.label_allgemein")}}</div>
</div>
{{#unless (ne actor.system.computed.num_allgemeine_sonderfertigkeiten 0)}}
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
{{/unless}}
{{#each actor.itemTypes.Sonderfertigkeit}}
{{#if (eq this.system.kategorie "allgemein")}}
<div class="list-item" data-item-id="{{_id}}">
<div>
<div class="fit-content" data-action="item-open">{{maybeLocalize name prefix=(concat "DSA41.sonderfertigkeiten." system.kategorie ".name")}}</div>
</div>
<div></div>
<div class="center fas fa-trash" data-action="item-delete"></div>
</div>
{{/if}}
{{/each}}
</div>
</div>
<div class="tab {{#if (eq tabGroups.primary 'talente')}}active{{/if}}" data-group="primary" data-tab="talente">
{{#each actor.system.talente}}
<div class="list subgrid-columns">
<div class="list-header">
<div class="center">{{localize (concat "DSA41.talente." @key ".label")}}</div>
<div class="center">{{localize "DSA41.talente.label_eigenschaften"}}</div>
<div class="center">{{localize "DSA41.talente.label_talentwert"}}</div>
</div>
{{#each this}}
<div class="list-item" data-item-id="{{_id}}">
<div data-action="item-open">{{maybeLocalize name prefix=(concat "DSA41.talente." system.kategorie ".name")}}</div>
<div class="center" data-action="roll" data-roll-type="talent">
{{>die-type type=system.attribute1}}
{{>die-type type=system.attribute2}}
{{>die-type type=system.attribute3}}
</div>
<div>{{>editable-input type="number" data-name="system.talentwert" value=system.talentwert}}</div>
<div class="center fas fa-trash" data-action="item-delete"></div>
</div>
{{/each}}
</div>
{{/each}}
<div class="list Kampftalente">
<div class="list-header">
<div class="center">{{localize "DSA41.talente.kampf.label"}}</div>
<div class="center">{{localize "DSA41.talente.label_talentwert"}}</div>
<div class="center">{{localize "DSA41.talente.kampf.label_attacke"}}</div>
<div class="center">{{localize "DSA41.talente.kampf.label_parade"}}</div>
<div class="center">{{localize "DSA41.talente.kampf.label_attacke_total"}}</div>
<div class="center">{{localize "DSA41.talente.kampf.label_parade_total"}}</div>
</div>
{{#each actor.system.kampftalente}}
<div class="list-item" data-item-id="{{_id}}">
<div data-action="item-open">{{maybeLocalize name prefix="DSA41.talente.kampf.name."}}</div>
<div>{{>editable-input type="number" name=(concat name "system.talentwert") data-name="system.talentwert" value=system.talentwert}}</div>
<div>{{>editable-input type="number" name=(concat name "system.attacke") data-name="system.attacke" value=system.attacke}}</div>
{{#if (ne system.kategorie "fernkampf")}}
<div>{{>editable-input type="number" name=(concat name "system.parade") data-name="system.parade" value=system.parade}}</div>
{{else}}
<div></div>
{{/if}}
<div class="center">{{lookup (lookup @root.actor.system.computed.kampf.talente name) "attacke"}}</div>
{{#if (ne system.kategorie "fernkampf")}}
<div class="center">{{lookup (lookup @root.actor.system.computed.kampf.talente name) "parade"}}</div>
{{else}}
<div></div>
{{/if}}
</div>
{{/each}}
</div>
</div>
<div class="tab {{#if (eq tabGroups.primary 'inventar')}}active{{/if}}" data-group="primary" data-tab="inventar">
<div class="currency">
{{DSA41_input "system.currency.dukaten"}}
<svg viewbox="80 0 40 40" data-tooltip="{{localize "DSA41.currency.dukaten"}}"> <use href="/systems/dsa-4th-edition/src/Assets/coins.svg#Gold"> </use></svg>
{{DSA41_input "system.currency.silbertaler"}}
<svg viewbox="120 0 40 40" data-tooltip="{{localize "DSA41.currency.silbertaler"}}"><use href="/systems/dsa-4th-edition/src/Assets/coins.svg#Diamond"></use></svg>
{{DSA41_input "system.currency.heller"}}
<svg viewbox="0 0 40 40" data-tooltip="{{localize "DSA41.currency.heller"}}"> <use href="/systems/dsa-4th-edition/src/Assets/coins.svg#Copper"> </use></svg>
{{DSA41_input "system.currency.kreuzer"}}
<svg viewbox="40 0 40 40" data-tooltip="{{localize "DSA41.currency.kreuzer"}}"> <use href="/systems/dsa-4th-edition/src/Assets/coins.svg#Silver"> </use></svg>
</div>
<div class="list Bewaffnung subgrid-columns">
<div class="list-header">
<div>{{localize "DSA41.inventar.bewaffnung"}}</div>
<div></div>
<div></div>
<div class="center">{{localize "DSA41.weight.label"}}</div>
<div></div>
</div>
{{#unless (ne actor.itemTypes.Bewaffnung.length 0)}}
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
{{/unless}}
{{#each (sorted actor.itemTypes.Bewaffnung)}}
<div class="list-item draggable" data-item-id="{{this._id}}">
<div class="row" data-action="item-open">
<img class="item-image" src="{{this.img}}" title="{{this.name}}">
<div class="col">
<span>{{this.name}}</span>
<span class="small">
{{#if this.system.nahkampfwaffe.aktiv}} Nahkampfwaffe {{/if}}
{{#if this.system.parierwaffe.aktiv}} Parierwaffe {{/if}}
{{#if this.system.schild.aktiv}} Schild {{/if}}
{{#if this.system.fernkampfwaffe.aktiv}} Fernkampfwaffe {{/if}}
</span>
</div>
</div>
<div></div>
<div class="center fas fa-sword" data-action="toggle_equipped" data-equipped="{{system.angelegt}}"></div>
<div class="center">{{this.system.gewicht.value}} {{localize (concat "DSA41.weight." this.system.gewicht.unit)}}</div>
<div class="center fas fa-trash" data-action="item-delete"></div>
</div>
{{/each}}
</div>
<div class="list Ruestung subgrid-columns">
<div class="list-header">
<div class="row">{{localize "DSA41.inventar.ruestungen"}}</div>
<div></div>
<div></div>
<div class="center">{{localize "DSA41.weight.label"}}</div>
</div>
{{#unless (ne actor.itemTypes.Ruestung.length 0)}}
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
{{/unless}}
{{#each (sorted actor.itemTypes.Ruestung)}}
<div class="list-item draggable" data-item-id="{{this._id}}">
<div class="row" data-action="item-open">
<img class="item-image" src="{{this.img}}" title="{{this.name}}">
<span class="center">{{this.name}}</span>
</div>
<div></div>
<div class="center fas fa-shield-halved" data-action="toggle_equipped" data-equipped="{{system.angelegt}}"></div>
<div class="center">{{this.system.gewicht.value}} {{localize (concat "DSA41.weight." this.system.gewicht.unit)}}</div>
<div class="center fas fa-trash" data-action="item-delete"></div>
</div>
{{/each}}
</div>
<div class="list Gegenstand subgrid-columns">
<div class="list-header">
<div>{{localize "DSA41.inventar.gegenstaende"}}</div>
<div></div>
<div></div>
<div class="center">{{localize "DSA41.weight.label"}}</div>
</div>
{{#unless (ne actor.itemTypes.Gegenstand.length 0)}}
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
{{/unless}}
{{#each (sorted actor.itemTypes.Gegenstand)}}
<div class="list-item draggable" data-item-id="{{this._id}}">
<div class="row" data-action="item-open">
<img class="item-image" src="{{this.img}}" title="{{this.name}}">
<span class="center">{{this.name}}</span>
</div>
<div></div>
<div></div>
<div class="center">{{this.system.gewicht.value}} {{localize (concat "DSA41.weight." this.system.gewicht.unit)}}</div>
<div class="center fas fa-trash" data-action="item-delete"></div>
</div>
{{/each}}
</div>
</div>
<div class="tab Kampf {{#if (eq tabGroups.primary 'kampf')}}active{{/if}}" data-group="primary" data-tab="kampf">
<div class="list Bewaffnung">
<div class="list-header ">
<div>{{localize "DSA41.kampf.bewaffnung"}}</div>
<div class="center">{{localize "DSA41.kampf.attacke"}}</div>
<div class="center">{{localize "DSA41.kampf.parade"}}</div>
<div class="center">{{localize "DSA41.kampf.trefferpunkte"}}</div>
</div>
{{#unless (or (ne actor.system.computed.num_waffen 0) (ne actor.system.computed.num_fernkampf_waffen 0))}}
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
{{/unless}}
{{#each actor.system.computed.kampf.waffen}}
<div class="list-item" data-item-id="{{item._id}}">
<div class="row" data-action="item-open">
<img class="item-image" src="{{item.img}}" title="{{item.name}}">
<div class="col">
<span>{{item.name}}</span>
</div>
</div>
<div class="center">{{>die-value type="attacke" data-roll="1d20" value=attacke success-value=attacke data-tooltip="attacke_tooltip"}}</div>
<div class="center">{{>die-value type="parade" data-roll="1d20" value=parade success-value=parade data-tooltip="parade_tooltip"}}</div>
<div class="center">{{>die-value type="trefferpunkte" data-roll=trefferpunkte value=trefferpunkte_display data-tooltip="trefferpunkte_tooltip"}}</div>
</div>
{{/each}}
{{#each actor.system.computed.kampf.fernkampf_waffen}}
<div class="list-item" data-item-id="{{item._id}}">
<div class="row" data-action="item-open">
<img class="item-image" src="{{item.img}}" title="{{item.name}}">
<div class="col">
<span>{{item.name}}</span>
</div>
</div>
<div class="center">{{>die-value type="fernkampf-attacke" data-roll="1d20" value=attacke data-tooltip="fernkampf_attacke_tooltip"}}</div>
<div class="center"></div>
<div class="center">{{>die-value type="fernkampf-trefferpunkte" data-roll=trefferpunkte value=trefferpunkte_display data-tooltip="fernkampf_trefferpunkte_tooltip"}}</div>
</div>
{{/each}}
</div>
<div class="list Ruestung">
<div class="list-header rowspan2">
<div class="rowspan2">{{localize "DSA41.kampf.ruestungen"}}</div>
<div class="rowspan2">{{localize "DSA41.ruestungen.kopf"}}</div>
<div class="rowspan2">{{localize "DSA41.ruestungen.brust"}}</div>
<div class="rowspan2">{{localize "DSA41.ruestungen.ruecken"}}</div>
<div class="rowspan2">{{localize "DSA41.ruestungen.bauch"}}</div>
<div class="colspan2 rowspan2 subgrid">
<div class="colspan2">{{localize "DSA41.ruestungen.arm"}}</div>
<div>{{localize "DSA41.ruestungen.links"}}</div>
<div>{{localize "DSA41.ruestungen.rechts"}}</div>
</div>
<div class="colspan2 rowspan2 subgrid">
<div class="colspan2">{{localize "DSA41.ruestungen.bein"}}</div>
<div>{{localize "DSA41.ruestungen.links"}}</div>
<div>{{localize "DSA41.ruestungen.rechts"}}</div>
</div>
<div class="colspan2 rowspan2 subgrid">
<div class="colspan2">{{localize "DSA41.ruestungen.gesamt"}}</div>
<div>{{localize "DSA41.ruestungen.ruestungsschutz"}}</div>
<div>{{localize "DSA41.ruestungen.behinderung"}}</div>
</div>
</div>
{{#each actor.system.computed.kampf.ruestungen}}
<div class="list-item" data-item-id="{{item._id}}">
<div class="row" data-action="item-open">
<img class="item-image" src="{{item.img}}" title="{{item.name}}">
<span class="center">{{item.name}}</span>
</div>
<div>{{item.system.kopf}}</div>
<div>{{item.system.brust}}</div>
<div>{{item.system.ruecken}}</div>
<div>{{item.system.bauch}}</div>
<div>{{item.system.linker_arm}}</div>
<div>{{item.system.rechter_arm}}</div>
<div>{{item.system.linkes_bein}}</div>
<div>{{item.system.rechtes_bein}}</div>
<div>{{item.system.gesamt_ruestungsschutz}}</div>
<div>{{item.system.gesamt_behinderung}}</div>
</div>
{{/each}}
<div class="list-item">
<div class="left">Total</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.kopf}}</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.brust}}</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.ruecken}}</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.bauch}}</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.linker_arm}}</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.rechter_arm}}</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.linkes_bein}}</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.rechtes_bein}}</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.gesamt_ruestungsschutz}}</div>
<div>{{actor.system.computed.kampf.ruestungen_gesamt.gesamt_behinderung}}</div>
</div>
</div>
<div class="list Sonderfertigkeiten">
<div class="list-header">
<div>{{localize (concat "DSA41.sonderfertigkeiten.label_kampf")}}</div>
</div>
{{#unless (ne actor.system.computed.num_kampf_sonderfertigkeiten 0)}}
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
{{/unless}}
{{#each actor.itemTypes.Sonderfertigkeit}}
{{#if (eq this.system.kategorie "kampf")}}
<div class="list-item" data-item-id="{{_id}}">
<div>
<div class="fit-content" data-action="item-open" data-tooltip="{{this.system.beschreibung}}">{{maybeLocalize name prefix=(concat "DSA41.sonderfertigkeiten." system.kategorie ".name")}}</div>
</div>
<div></div>
<div class="center fas fa-trash" data-action="item-delete"></div>
</div>
{{/if}}
{{/each}}
</div>
</div>
<div class="tab Zauber {{#if (eq tabGroups.primary 'zauber')}}active{{/if}}" data-group="primary" data-tab="zauber">
<div class="list Zauber subgrid-columns">
<div class="list-header ">
<div>{{localize "DSA41.kampf.zauber"}}</div>
<div></div>
<div class="center">Eigenschaften</div>
<div></div>
<div class="center">{{localize "DSA41.zauber.label_zauberfertigkeitswert"}}</div>
<div></div>
</div>
{{#unless (ne actor.itemTypes.Zauber.length 0)}}
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
{{/unless}}
{{#each actor.itemTypes.Zauber}}
<div class="list-item" data-item-id="{{_id}}">
<div class="row" data-action="item-open" data-tooltip-direction="LEFT" data-tooltip="{{system.beschreibung}}">
<img class="item-image" src="{{img}}" title="{{name}}">
<div class="col">
<span>{{name}}</span>
</div>
</div>
<div></div>
<div class="center" data-action="roll" data-roll-type="zauber">
{{>die-type type=system.attribute1}}
{{>die-type type=system.attribute2}}
{{>die-type type=system.attribute3}}
</div>
<div></div>
<div>{{>editable-input type="number" data-name="system.zauberfertigkeitswert" value=system.zauberfertigkeitswert}}</div>
<div class="center fas fa-trash" data-action="item-delete"></div>
</div>
{{/each}}
</div>
</div>
</div>
</div>
</div>
-73
View File
@@ -1,73 +0,0 @@
{{#*inline "editable-input"}}
<div class="editable-input editable-{{type}}">
{{#if @root.editable}}
<input type="{{type}}" name="{{name}}" value="{{value}}" placeholder="{{placeholder}}">
{{else}}
<div>
{{value}}
{{#unless value}}{{placeholder}}{{/unless}}
</div>
{{/if}}
{{#if placeholder}}
<div class="placeholder">{{placeholder}}</div>
{{/if}}
</div>
{{/inline}}
{{#*inline "die-type"}}
<div class="center die die-{{type}}">{{localize (concat "DSA41.attributes.short." type)}}</div>
{{/inline}}
{{#*inline "die-value"}}
<div class="col">
<div class="center">{{localize (concat "DSA41.attributes.short." type)}}</div>
<div class="die die-{{type}}">{{lookup @root.actor.system.computed type}}</div>
</div>
{{/inline}}
<form class="actor-sheet {{ cssClass }}" autocomplete="off">
<div class="row">
{{>editable-input type="text" name="name" value=actor.name placeholder=(localize "DSA41.name")}}
{{>editable-input type="text" name="system.race" value=actor.system.race placeholder=(localize "DSA41.race")}}
{{>editable-input type="text" name="system.culture" value=actor.system.culture placeholder=(localize "DSA41.culture")}}
{{>editable-input type="text" name="system.profession" value=actor.system.profession placeholder=(localize "DSA41.profession")}}
</div>
<div class="row">
<img class="character-image" src="{{ actor.img }}" title="{{ actor.name }}" {{#if editable}}data-edit="img"{{/if}}>
{{#each actor.system.attributes}}
{{>die-value type=@key}}
{{/each}}
</div>
<div class="row">
<fieldset>
<legend>{{localize "DSA41.attributes.label"}}</legend>
<table>
<tr>
<th></th>
{{#each actor.system.attributes}}
<th>{{localize (concat "DSA41.attributes.short." @key)}}</th>
{{/each}}
</tr>
<tr>
<td>{{localize "DSA41.attributes.initial"}}</td>
{{#each actor.system.attributes}}
<td>{{>editable-input type="number" name=(concat "system.attributes." @key ".initial") value=(lookup this "initial")}}</td>
{{/each}}
</tr>
<tr>
<td>{{localize "DSA41.attributes.advancement"}}</td>
{{#each actor.system.attributes}}
<td>{{>editable-input type="number" name=(concat "system.attributes." @key ".advancement") value=(lookup this "advancement")}}</td>
{{/each}}
</tr>
<tr>
<td>{{localize "DSA41.attributes.modifier"}}</td>
{{#each actor.system.attributes}}
<td>{{>editable-input type="number" name=(concat "system.attributes." @key ".modifier") value=(lookup this "modifier")}}</td>
{{/each}}
</tr>
</table>
</fieldset>
</div>
</form>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="160" height="40" version="1.1" viewBox="0 0 160 40" xmlns="http://www.w3.org/2000/svg">
<g id="Diamond">
<path fill="#5796a1" d="M 154.15,5.9 H 154.1 Q 148.25,0 140,0 131.65,0 125.85,5.9 120.8,10.95 120.1,17.75 120,18.9 120,20 q 0,0.5 0.05,0.95 0,0.05 0,0.1 0,0.6 0.05,1.25 0.7,6.8 5.75,11.9 v 0.05 Q 131.7,40 140,40 q 8.25,0 14.1,-5.75 h 0.05 q 5.45,-5.55 5.85,-13.1 0,-0.05 0,-0.1 0,-0.05 0,-0.15 0,-0.4 0,-0.9 0,-8.25 -5.85,-14.1 m -26.9,1.4 Q 132.5,2 140,2 q 7.45,0 12.7,5.3 5.3,5.25 5.3,12.7 0,0.5 0,0.9 0,0.1 0,0.15 -0.35,6.8 -5.3,11.75 -5.25,5.2 -12.7,5.2 -7.5,0 -12.75,-5.2 -4.5,-4.6 -5.15,-10.7 -0.05,-0.55 -0.05,-1.05 0,-0.1 0,-0.15 Q 122,20.5 122,20 q 0,-1 0.1,-2.05 0.65,-6.1 5.15,-10.65 z"/>
<path fill="#b6ecf5" d="m 140,4.1 q -7.5,0 -12.75,5.3 -4.5,4.5 -5.15,10.7 -0.05,0.4 -0.05,0.8 0,0.05 0,0.15 0,0.5 0.05,1.05 0.65,6.1 5.15,10.7 5.25,5.2 12.75,5.2 7.45,0 12.7,-5.2 4.95,-4.95 5.3,-11.75 Q 158,21 158,20.9 157.6,14.2 152.7,9.4 147.45,4.1 140,4.1 m -7.65,8.3 q 3.15,-3.2 7.65,-3.2 4.5,0 7.65,3.2 3.15,3.15 3.15,7.6 0,0.35 0,0.65 0,0.15 0,0.25 0,0.15 0,0.25 -0.4,3.75 -3.15,6.55 -3.15,3.1 -7.65,3.1 -4.5,0 -7.65,-3.1 -2.9,-2.9 -3.1,-6.8 v -0.05 q 0,-0.1 0,-0.2 -0.05,-0.3 -0.05,-0.65 0,-4.45 3.15,-7.6 m 7.7,1.4 H 140 q -0.85,0 -1.55,0.6 -0.6,0.7 -0.6,1.55 v 8.15 q 0,0.85 0.6,1.5 0.7,0.65 1.55,0.65 h 0.05 q 0.9,0 1.5,-0.65 0.7,-0.65 0.7,-1.5 v -8.15 q 0,-0.85 -0.7,-1.55 -0.6,-0.6 -1.5,-0.6 z"/>
<path fill="#9cd6e0" d="m 140,11.3 q -4.5,0 -7.65,3.15 -2.65,2.7 -3.1,6.4 v 0.05 q 0.2,3.9 3.1,6.8 3.15,3.1 7.65,3.1 4.5,0 7.65,-3.1 2.75,-2.8 3.15,-6.55 0,-0.1 0,-0.25 -0.45,-3.7 -3.15,-6.45 Q 144.5,11.3 140,11.3 m 0,2.5 h 0.05 q 0.9,0 1.5,0.6 0.7,0.7 0.7,1.55 v 8.15 q 0,0.85 -0.7,1.5 -0.6,0.65 -1.5,0.65 H 140 q -0.85,0 -1.55,-0.65 -0.6,-0.65 -0.6,-1.5 v -8.15 q 0,-0.85 0.6,-1.55 0.7,-0.6 1.55,-0.6 z"/>
<path fill="#80c2cd" d="m 140,9.2 q -4.5,0 -7.65,3.2 -3.15,3.15 -3.15,7.6 0,0.35 0.05,0.65 0,0.1 0,0.2 0.45,-3.7 3.1,-6.4 3.15,-3.15 7.65,-3.15 4.5,0 7.65,3.15 2.7,2.75 3.15,6.45 0,-0.1 0,-0.25 0,-0.3 0,-0.65 0,-4.45 -3.15,-7.6 Q 144.5,9.2 140,9.2 Z"/>
<path fill="#d0f8ff" d="m 140,2 q -7.5,0 -12.75,5.3 -4.5,4.55 -5.15,10.65 Q 122,19 122,20 q 0,0.5 0.05,0.9 0,-0.4 0.05,-0.8 0.65,-6.2 5.15,-10.7 5.25,-5.3 12.75,-5.3 7.45,0 12.7,5.3 4.9,4.8 5.3,11.5 0,-0.4 0,-0.9 0,-7.45 -5.3,-12.7 Q 147.45,2 140,2 Z"/>
</g>
<g id="Silver">
<path fill="#778b8c" d="M 80,21.05 Q 80,21 80,20.9 80,20.5 80,20 80,11.75 74.15,5.9 H 74.1 Q 68.25,0 60,0 51.65,0 45.85,5.9 40.8,10.95 40.1,17.75 40,18.9 40,20 q 0,0.5 0.05,0.95 0,0.05 0,0.1 0,0.6 0.05,1.25 0.7,6.8 5.75,11.9 v 0.05 q 5.76172,5.66328 13.9,5.75 0.1248,0 0.25,0 8.25,0 14.1,-5.75 h 0.05 q 3.31836,-3.3793 4.75,-7.5 0.94355,-2.64707 1.1,-5.6 0,-0.05 0,-0.1 M 60,2 q 7.45,0 12.7,5.3 5.3,5.25 5.3,12.7 0,0.5 0,0.9 0,0.1 0,0.15 -0.15625,3.03437 -1.25,5.7 Q 75.44121,30.05879 72.7,32.8 67.45,38 60,38 q -0.1252,0 -0.25,0 -7.3377,-0.0867 -12.5,-5.2 -4.5,-4.6 -5.15,-10.7 -0.05,-0.55 -0.05,-1.05 0,-0.1 0,-0.15 Q 42,20.5 42,20 42,19 42.1,17.95 42.75,11.85 47.25,7.3 52.5,2 60,2 Z"/>
<path fill="#c7d4d4" d="m 60,4.1 q 7.45,0 12.7,5.3 4.9,4.8 5.3,11.5 Q 78,20.5 78,20 78,12.55 72.7,7.3 67.45,2 60,2 52.5,2 47.25,7.3 42.75,11.85 42.1,17.95 42,19 42,20 q 0,0.5 0.05,0.9 0,-0.4 0.05,-0.8 Q 42.75,13.9 47.25,9.4 52.5,4.1 60,4.1 Z"/>
<path fill="#acc0c1" d="M 72.7,9.4 Q 67.45,4.1 60,4.1 q -7.5,0 -12.75,5.3 -4.5,4.5 -5.15,10.7 -0.05,0.4 -0.05,0.8 0,0.05 0,0.15 0,0.5 0.05,1.05 0.65,6.1 5.15,10.7 5.1623,5.11328 12.5,5.2 0.1248,0 0.25,0 7.45,0 12.7,-5.2 2.74121,-2.74121 4.05,-6.05 Q 77.84375,24.08437 78,21.05 78,21 78,20.9 77.6,14.2 72.7,9.4 M 60,9.2 q 4.5,0 7.65,3.2 3.15,3.15 3.15,7.6 0,0.35 0,0.65 0,0.15 0,0.25 0,0.15 0,0.25 -0.33398,3.13125 -2.3,5.6 -0.39629,0.48789 -0.85,0.95 -3.15,3.1 -7.65,3.1 -0.12539,0 -0.25,0 -4.33789,-0.0863 -7.4,-3.1 -2.9,-2.9 -3.1,-6.8 v -0.05 q 0,-0.1 0,-0.2 Q 49.2,20.35 49.2,20 49.2,15.55 52.35,12.4 55.5,9.2 60,9.2 m 0.05,4.6 H 60 q -0.85,0 -1.55,0.6 -0.6,0.7 -0.6,1.55 v 8.15 q 0,0.85 0.6,1.5 0.7,0.65 1.55,0.65 h 0.05 q 0.9,0 1.5,-0.65 0.7,-0.65 0.7,-1.5 v -8.15 q 0,-0.85 -0.7,-1.55 -0.6,-0.6 -1.5,-0.6 z"/>
<path fill="#95aead" d="M 67.65,14.45 Q 64.5,11.3 60,11.3 q -4.5,0 -7.65,3.15 -2.65,2.7 -3.1,6.4 v 0.05 q 0.2,3.9 3.1,6.8 3.06211,3.01367 7.4,3.1 0.12461,0 0.25,0 4.5,0 7.65,-3.1 0.45371,-0.46211 0.85,-0.95 1.96602,-2.46875 2.3,-5.6 0,-0.1 0,-0.25 -0.45,-3.7 -3.15,-6.45 M 60,13.8 h 0.05 q 0.9,0 1.5,0.6 0.7,0.7 0.7,1.55 v 8.15 q 0,0.85 -0.7,1.5 -0.6,0.65 -1.5,0.65 H 60 q -0.85,0 -1.55,-0.65 -0.6,-0.65 -0.6,-1.5 v -8.15 q 0,-0.85 0.6,-1.55 0.7,-0.6 1.55,-0.6 z"/>
<path fill="#829f9f" d="m 60,11.3 q 4.5,0 7.65,3.15 2.7,2.75 3.15,6.45 0,-0.1 0,-0.25 0,-0.3 0,-0.65 0,-4.45 -3.15,-7.6 Q 64.5,9.2 60,9.2 q -4.5,0 -7.65,3.2 -3.15,3.15 -3.15,7.6 0,0.35 0.05,0.65 0,0.1 0,0.2 0.45,-3.7 3.1,-6.4 Q 55.5,11.3 60,11.3 Z"/>
</g>
<g id="Copper">
<path fill="#85572c" d="m 40.0002,20.9 q 0,-0.4 0,-0.9 -0.004,-8.26797 -5.9,-14.1 V 5.85 Q 28.26797,0.00352 20.0002,0 q -8.3377,0.005 -14.2,5.9 -4.98223,5.06113 -5.7,11.85 -0.10469,1.15234 -0.1,2.25 0.004,0.51504 0.05,0.95 0,0.0391 0,0.1 -0.004,0.58301 0.05,1.2 v 0.05 q 0.71973,6.78516 5.7,11.9 h 0.05 q 5.82656,5.79746 14.15,5.8 8.27148,-3.9e-4 14.1,-5.8 v 0 q 5.52207,-5.48301 5.9,-13.05 10e-4,-0.0559 0,-0.1 0,-0.05 0,-0.15 m -20,-18.9 q 7.45,0 12.7,5.3 5.3,5.25 5.3,12.7 0,0.5 0,0.9 0,0.1 0,0.15 -0.35,6.8 -5.3,11.75 -5.25,5.2 -12.7,5.2 -7.5,0 -12.75,-5.2 -4.5,-4.6 -5.15,-10.7 -0.05,-0.55 -0.05,-1.05 0,-0.1 0,-0.15 -0.05,-0.4 -0.05,-0.9 0,-1 0.1,-2.05 0.65,-6.1 5.15,-10.65 5.25,-5.3 12.75,-5.3 z"/>
<path fill="#cca277" d="m 20.0002,4.1 q 7.45,0 12.7,5.3 4.9,4.8 5.3,11.5 0,-0.4 0,-0.9 0,-7.45 -5.3,-12.7 -5.25,-5.3 -12.7,-5.3 -7.5,0 -12.75,5.3 -4.5,4.55 -5.15,10.65 -0.1,1.05 -0.1,2.05 0,0.5 0.05,0.9 0,-0.4 0.05,-0.8 0.65,-6.2 5.15,-10.7 5.25,-5.3 12.75,-5.3 z"/>
<path fill="#bf8851" d="m 32.7002,9.4 q -5.25,-5.3 -12.7,-5.3 -7.5,0 -12.75,5.3 -4.5,4.5 -5.15,10.7 -0.05,0.4 -0.05,0.8 0,0.05 0,0.15 0,0.5 0.05,1.05 0.65,6.1 5.15,10.7 5.25,5.2 12.75,5.2 7.45,0 12.7,-5.2 4.95,-4.95 5.3,-11.75 0,-0.05 0,-0.15 -0.4,-6.7 -5.3,-11.5 m -12.7,-0.2 q 4.5,0 7.65,3.2 3.15,3.15 3.15,7.6 0,0.35 0,0.65 0,0.15 0,0.25 0,0.15 0,0.25 -0.4,3.75 -3.15,6.55 -3.15,3.1 -7.65,3.1 -4.5,0 -7.65,-3.1 -2.9,-2.9 -3.1,-6.8 v -0.05 q 0,-0.1 0,-0.2 -0.05,-0.3 -0.05,-0.65 0,-4.45 3.15,-7.6 3.15,-3.2 7.65,-3.2 m 1.55,5.2 q -0.6,-0.6 -1.5,-0.6 h -0.05 q -0.85,0 -1.55,0.6 -0.6,0.7 -0.6,1.55 v 8.15 q 0,0.85 0.6,1.5 0.7,0.65 1.55,0.65 h 0.05 q 0.9,0 1.5,-0.65 0.7,-0.65 0.7,-1.5 v -8.15 q 0,-0.85 -0.7,-1.55 z"/>
<path fill="#ae7640" d="m 27.6502,14.45 q -3.15,-3.15 -7.65,-3.15 -4.5,0 -7.65,3.15 -2.65,2.7 -3.1,6.4 v 0.05 q 0.2,3.9 3.1,6.8 3.15,3.1 7.65,3.1 4.5,0 7.65,-3.1 2.75,-2.8 3.15,-6.55 0,-0.1 0,-0.25 -0.45,-3.7 -3.15,-6.45 m -7.6,-0.65 q 0.9,0 1.5,0.6 0.7,0.7 0.7,1.55 v 8.15 q 0,0.85 -0.7,1.5 -0.6,0.65 -1.5,0.65 h -0.05 q -0.85,0 -1.55,-0.65 -0.6,-0.65 -0.6,-1.5 v -8.15 q 0,-0.85 0.6,-1.55 0.7,-0.6 1.55,-0.6 z"/>
<path fill="#9c6938" d="m 27.6502,12.4 q -3.15,-3.2 -7.65,-3.2 -4.5,0 -7.65,3.2 -3.15,3.15 -3.15,7.6 0,0.35 0.05,0.65 0,0.1 0,0.2 0.45,-3.7 3.1,-6.4 3.15,-3.15 7.65,-3.15 4.5,0 7.65,3.15 2.7,2.75 3.15,6.45 0,-0.1 0,-0.25 0,-0.3 0,-0.65 0,-4.45 -3.15,-7.6 z"/>
</g>
<g id="Gold">
<path fill="#af8c00" d="M 114.15,5.9 H 114.1 Q 108.25,0 100,0 91.65,0 85.85,5.9 80.8,10.95 80.1,17.75 80,18.9 80,20 q 0,0.5 0.05,0.95 0,0.05 0,0.1 0,0.6 0.05,1.25 0.7,6.8 5.75,11.9 v 0.05 Q 91.7,40 100,40 q 8.25,0 14.1,-5.75 h 0.05 q 5.45,-5.55 5.85,-13.1 0,-0.05 0,-0.1 0,-0.05 0,-0.15 0,-0.4 0,-0.9 0,-8.25 -5.85,-14.1 M 87.25,7.3 Q 92.5,2 100,2 q 7.45,0 12.7,5.3 5.3,5.25 5.3,12.7 0,0.5 0,0.9 0,0.1 0,0.15 -0.35,6.8 -5.3,11.75 -5.25,5.2 -12.7,5.2 -7.5,0 -12.75,-5.2 -4.5,-4.6 -5.15,-10.7 -0.05,-0.55 -0.05,-1.05 0,-0.1 0,-0.15 Q 82,20.5 82,20 q 0,-1 0.1,-2.05 0.65,-6.1 5.15,-10.65 z"/>
<path fill="#ffcc00" d="m 100,4.1 q -7.5,0 -12.75,5.3 -4.5,4.5 -5.15,10.7 -0.05,0.4 -0.05,0.8 0,0.05 0,0.15 0,0.5 0.05,1.05 0.65,6.1 5.15,10.7 5.25,5.2 12.75,5.2 7.45,0 12.7,-5.2 4.95,-4.95 5.3,-11.75 Q 118,21 118,20.9 117.6,14.2 112.7,9.4 107.45,4.1 100,4.1 m -7.65,8.3 Q 95.5,9.2 100,9.2 q 4.5,0 7.65,3.2 3.15,3.15 3.15,7.6 0,0.35 0,0.65 0,0.15 0,0.25 0,0.15 0,0.25 -0.4,3.75 -3.15,6.55 -3.15,3.1 -7.65,3.1 -4.5,0 -7.65,-3.1 -2.9,-2.9 -3.1,-6.8 v -0.05 q 0,-0.1 0,-0.2 -0.05,-0.3 -0.05,-0.65 0,-4.45 3.15,-7.6 m 7.65,1.4 q -0.85,0 -1.55,0.6 -0.6,0.7 -0.6,1.55 v 8.15 q 0,0.85 0.6,1.5 0.7,0.65 1.55,0.65 h 0.05 q 0.9,0 1.5,-0.65 0.7,-0.65 0.7,-1.5 v -8.15 q 0,-0.85 -0.7,-1.55 -0.6,-0.6 -1.5,-0.6 z"/>
<path fill="#e3b602" d="m 100,11.3 q -4.5,0 -7.65,3.15 -2.65,2.7 -3.1,6.4 v 0.05 q 0.2,3.9 3.1,6.8 3.15,3.1 7.65,3.1 4.5,0 7.65,-3.1 2.75,-2.8 3.15,-6.55 0,-0.1 0,-0.25 -0.45,-3.7 -3.15,-6.45 Q 104.5,11.3 100,11.3 m -1.55,3.1 q 0.7,-0.6 1.55,-0.6 h 0.05 q 0.9,0 1.5,0.6 0.7,0.7 0.7,1.55 v 8.15 q 0,0.85 -0.7,1.5 -0.6,0.65 -1.5,0.65 H 100 q -0.85,0 -1.55,-0.65 -0.6,-0.65 -0.6,-1.5 v -8.15 q 0,-0.85 0.6,-1.55 z"/>
<path fill="#caa202" d="M 92.35,14.45 Q 95.5,11.3 100,11.3 q 4.5,0 7.65,3.15 2.7,2.75 3.15,6.45 0,-0.1 0,-0.25 0,-0.3 0,-0.65 0,-4.45 -3.15,-7.6 -3.15,-3.2 -7.65,-3.2 -4.5,0 -7.65,3.2 -3.15,3.15 -3.15,7.6 0,0.35 0.05,0.65 0,0.1 0,0.2 0.45,-3.7 3.1,-6.4 z"/>
<path fill="#fee481" d="M 100,2 Q 92.5,2 87.25,7.3 82.75,11.85 82.1,17.95 82,19 82,20 q 0,0.5 0.05,0.9 0,-0.4 0.05,-0.8 0.65,-6.2 5.15,-10.7 5.25,-5.3 12.75,-5.3 7.45,0 12.7,5.3 4.9,4.8 5.3,11.5 0,-0.4 0,-0.9 0,-7.45 -5.3,-12.7 Q 107.45,2 100,2 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<g>
<g id="d20">
<path d="m11.906 20.4h40.188l-20.094 34.801z"/>
<path d="m52.635 21.059 6.5977 26.664-26.391 7.6172z"/>
<path d="m32 .55664 19.791 19.043h-39.582z"/>

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 705 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<g id="plus">
<rect style="fill:#a59481" id="rect1" width="62" height="8" x="1" y="28" />
<rect style="fill:#a59481" id="rect2" width="8" height="62" x="28" y="1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 328 B

+7
View File
@@ -0,0 +1,7 @@
<div class="DSA41 row chat-header">
<img src="{{img}}" alt="{{name}}">
<div class="col">
<span class="title">{{name}}</span>
<span class="subtitle">{{author}}</span>
</div>
</div>
+39
View File
@@ -0,0 +1,39 @@
<div class="talent_chat_message">
<h3>{{talent.name}} ({{localize (concat "DSA41.chat.talentwert_short")}}: {{talent.system.talentwert}}{{#if (ne modifikator 0)}} + {{modifikator}}{{/if}})</h3>
<div class="info">
<div>
<div>{{localize (concat "DSA41.chat.attribute")}}</div>
<div>{{localize (concat "DSA41.chat.value")}}</div>
<div>{{localize (concat "DSA41.chat.roll")}}</div>
<div>{{localize (concat "DSA41.chat.talentwert_short")}}</div>
</div>
<div>
<div>{{localize (concat "DSA41.attributes.long." attribute1.type)}}</div>
<div>{{attribute1.value}}</div>
<div>{{roll1}}</div>
<div>{{needed_taw_roll1}}</div>
</div>
<div>
<div>{{localize (concat "DSA41.attributes.long." attribute2.type)}}</div>
<div>{{attribute2.value}}</div>
<div>{{roll2}}</div>
<div>{{needed_taw_roll2}}</div>
</div>
<div>
<div>{{localize (concat "DSA41.attributes.long." attribute3.type)}}</div>
<div>{{attribute3.value}}</div>
<div>{{roll3}}</div>
<div>{{needed_taw_roll3}}</div>
</div>
</div>
<div>
{{localize (concat "DSA41.chat.result")}}:
{{#if (lt leftover_taw 0)}}
<b>{{localize (concat "DSA41.chat.failure")}}</b>
{{else}}
<b>{{localize (concat "DSA41.chat.success")}}</b>
{{/if}}
({{localize (concat "DSA41.chat.talentwert_short")}}: {{leftover_taw}})
</div>
</div>
+11
View File
@@ -0,0 +1,11 @@
<div class="DSA41 chat-targets">
<div class="center">{{localize "DSA41.chat.targets"}}</div>
{{#each this}}
<div class="target" data-actor-id="{{uuid}}">
<img src="{{img}}" alt="{{name}}">
<span>{{name}}</span>
<button data-action="apply_damage">{{localize "DSA41.chat.trefferpunkte_apply"}}</button>
</div>
{{/each}}
</div>
+39
View File
@@ -0,0 +1,39 @@
<div class="zauber_chat_message">
<h3>{{zauber.name}} ({{localize (concat "DSA41.chat.zauberfertigkeitswert_short")}}: {{zauber.system.zauberfertigkeitswert}}{{#if (ne modifikator 0)}} + {{modifikator}}{{/if}}{{#if (ne magieresistenz 0)}} + {{magieresistenz}}{{/if}})</h3>
<div class="info">
<div>
<div>{{localize (concat "DSA41.chat.attribute")}}</div>
<div>{{localize (concat "DSA41.chat.value")}}</div>
<div>{{localize (concat "DSA41.chat.roll")}}</div>
<div>{{localize (concat "DSA41.chat.zauberfertigkeitswert_short")}}</div>
</div>
<div>
<div>{{localize (concat "DSA41.attributes.long." attribute1.type)}}</div>
<div>{{attribute1.value}}</div>
<div>{{roll1}}</div>
<div>{{needed_zfw_roll1}}</div>
</div>
<div>
<div>{{localize (concat "DSA41.attributes.long." attribute2.type)}}</div>
<div>{{attribute2.value}}</div>
<div>{{roll2}}</div>
<div>{{needed_zfw_roll2}}</div>
</div>
<div>
<div>{{localize (concat "DSA41.attributes.long." attribute3.type)}}</div>
<div>{{attribute3.value}}</div>
<div>{{roll3}}</div>
<div>{{needed_zfw_roll3}}</div>
</div>
</div>
<div>
{{localize (concat "DSA41.chat.result")}}:
{{#if (lt leftover_zfw 0)}}
<b>{{localize (concat "DSA41.chat.failure")}}</b>
{{else}}
<b>{{localize (concat "DSA41.chat.success")}}</b>
{{/if}}
({{localize (concat "DSA41.chat.zauberfertigkeitswert_short")}}: {{leftover_zfw}})
</div>
</div>
+28
View File
@@ -0,0 +1,28 @@
<div>
<span class="colspan2">{{localize "DSA41.kampf.modifikator"}}</span>
<input class="colspan2" type="number" name="modifikator" value="{{lookup formData "modifikator"}}">
<div class="dsa41-calculation colspan4 center">
<ruby>{{options.item.basis_attacke}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.basis"}}</rt></ruby>
+ <ruby>{{options.item.talent_attacke}}<rt>{{localize "DSA41.talente.label"}}</rt></ruby>
{{#if (ne options.item.modifikator_attacke 0)}}
+ <ruby>{{options.item.modifikator_attacke}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.modifikator"}}</rt></ruby>
{{/if}}
{{#if (ne options.item.parierwaffe_attacke 0)}}
+ <ruby>{{options.item.parierwaffe_attacke}}<rt>{{localize "DSA41.bewaffnung.parierwaffe.label"}}</rt></ruby>
{{/if}}
{{#if (ne options.item.schild_attacke 0)}}
+ <ruby>{{options.item.schild_attacke}}<rt>{{localize "DSA41.bewaffnung.schild.label"}}</rt></ruby>
{{/if}}
{{#if (lt options.item.tp_kk 0)}}
+ <ruby>{{options.item.tp_kk}}<rt>{{localize "DSA41.attributes.long.strength"}}</rt></ruby>
{{/if}}
+ <ruby>{{formData.modifikator}}<rt>{{localize "DSA41.kampf.modifikator"}}</rt></ruby>
</div>
</div>
+16
View File
@@ -0,0 +1,16 @@
<div>
<span class="colspan2">{{localize "DSA41.kampf.modifikator"}}</span>
<input class="colspan2" type="number" name="modifikator" value="{{lookup formData "modifikator"}}">
<div class="dsa41-calculation colspan4 center">
<ruby>{{options.attribute.initial}}<rt>{{localize "DSA41.attributes.initial"}}</rt></ruby>
{{#if (ne options.attribute.advancement 0)}}
+ <ruby>{{options.attribute.advancement}}<rt>{{localize "DSA41.attributes.advancement"}}</rt></ruby>
{{/if}}
{{#if (ne options.attribute.modifier 0)}}
+ <ruby>{{options.attribute.modifier}}<rt>{{localize "DSA41.attributes.modifier"}}</rt></ruby>
{{/if}}
+ <ruby>{{formData.modifikator}}<rt>{{localize "DSA41.kampf.modifikator"}}</rt></ruby>
</div>
</div>
+59
View File
@@ -0,0 +1,59 @@
<div>
<span>{{localize "DSA41.kampf.zielgroesse.label"}}</span>
<select name="ziel_groesse">
<option value="-2" {{#if (eq formData.ziel_groesse -2)}}selected{{/if}}>{{localize "DSA41.kampf.zielgroesse.sehr_gross"}}</option>
<option value="0" {{#if (eq formData.ziel_groesse 0)}}selected{{/if}}>{{localize "DSA41.kampf.zielgroesse.gross"}} </option>
<option value="2" {{#if (eq formData.ziel_groesse 2)}}selected{{/if}}>{{localize "DSA41.kampf.zielgroesse.mittel"}} </option>
<option value="4" {{#if (eq formData.ziel_groesse 4)}}selected{{/if}}>{{localize "DSA41.kampf.zielgroesse.klein"}} </option>
<option value="6" {{#if (eq formData.ziel_groesse 6)}}selected{{/if}}>{{localize "DSA41.kampf.zielgroesse.sehr_klein"}}</option>
<option value="8" {{#if (eq formData.ziel_groesse 8)}}selected{{/if}}>{{localize "DSA41.kampf.zielgroesse.winzig"}} </option>
</select>
<span>{{localize "DSA41.kampf.deckung.label"}}</span>
<select name="deckung">
<option value="0" {{#if (eq formData.deckung 0)}}selected{{/if}}>{{localize "DSA41.kampf.deckung.keine"}} </option>
<option value="2" {{#if (eq formData.deckung 2)}}selected{{/if}}>{{localize "DSA41.kampf.deckung.halb"}} </option>
<option value="4" {{#if (eq formData.deckung 4)}}selected{{/if}}>{{localize "DSA41.kampf.deckung.drei_viertel"}}</option>
</select>
<span>{{localize "DSA41.kampf.entfernung.label"}}</span>
<select name="entfernung">
<option value="-2" {{#if (eq formData.entfernung -2)}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.sehr_nah"}} </option>
<option value="0" {{#if (eq formData.entfernung 0)}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.nah"}} </option>
<option value="4" {{#if (eq formData.entfernung 4)}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.mittel"}} </option>
<option value="8" {{#if (eq formData.entfernung 8)}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.weit"}} </option>
<option value="12" {{#if (eq formData.entfernung 12)}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.extrem_weit"}}</option>
</select>
<span>{{localize "DSA41.kampf.zielbewegung.label"}}</span>
<select name="ziel_bewegung">
<option value="-4" {{#if (eq formData.ziel_bewegung -4)}}selected{{/if}}>{{localize "DSA41.kampf.zielbewegung.unbeweglich"}} </option>
<option value="-2" {{#if (eq formData.ziel_bewegung -2)}}selected{{/if}}>{{localize "DSA41.kampf.zielbewegung.stillstehend"}}</option>
<option value="0" {{#if (eq formData.ziel_bewegung 0)}}selected{{/if}}>{{localize "DSA41.kampf.zielbewegung.leicht"}} </option>
<option value="2" {{#if (eq formData.ziel_bewegung 2)}}selected{{/if}}>{{localize "DSA41.kampf.zielbewegung.schnell"}} </option>
<option value="4" {{#if (eq formData.ziel_bewegung 4)}}selected{{/if}}>{{localize "DSA41.kampf.zielbewegung.sehr_schnell"}}</option>
</select>
<span>{{localize "DSA41.kampf.wind.label"}}</span>
<select name="wind">
<option value="0" {{#if (eq formData.wind 0)}}selected{{/if}}>{{localize "DSA41.kampf.wind.still"}} </option>
<option value="4" {{#if (eq formData.wind 4)}}selected{{/if}}>{{localize "DSA41.kampf.wind.seitenwind"}} </option>
<option value="8" {{#if (eq formData.wind 8)}}selected{{/if}}>{{localize "DSA41.kampf.wind.starker_seitenwind"}}</option>
</select>
<span>{{localize "DSA41.kampf.modifikator"}}</span>
<input type="number" name="modifikator" value="{{lookup formData "modifikator"}}">
<div class="dsa41-calculation colspan4 center">
max(-2,
<ruby>{{formData.ziel_groesse}}<rt>{{localize "DSA41.kampf.zielgroesse.label"}}</rt></ruby>
+ <ruby>{{formData.deckung}}<rt>{{localize "DSA41.kampf.deckung.label"}}</rt></ruby>
+ <ruby>{{formData.ziel_bewegung}}<rt>{{localize "DSA41.kampf.zielbewegung.label"}}</rt></ruby>
)
+ <ruby>{{formData.entfernung}}<rt>{{localize "DSA41.kampf.entfernung.label"}}</rt></ruby>
+ <ruby>{{formData.wind}}<rt>{{localize "DSA41.kampf.wind.label"}}</rt></ruby>
+ <ruby>{{formData.modifikator}}<rt>{{localize "DSA41.kampf.modifikator"}}</rt></ruby>
</div>
</div>
+24
View File
@@ -0,0 +1,24 @@
<div>
<span>{{localize "DSA41.kampf.entfernung.label"}}</span>
<select name="entfernung">
<option value="modifikator1" {{#if (eq formData.entfernung "modifikator1")}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.sehr_nah"}} </option>
<option value="modifikator2" {{#if (eq formData.entfernung "modifikator2")}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.nah"}} </option>
<option value="modifikator3" {{#if (eq formData.entfernung "modifikator3")}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.mittel"}} </option>
<option value="modifikator4" {{#if (eq formData.entfernung "modifikator4")}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.weit"}} </option>
<option value="modifikator5" {{#if (eq formData.entfernung "modifikator5")}}selected{{/if}}>{{localize "DSA41.kampf.entfernung.extrem_weit"}}</option>
</select>
<span>{{localize "DSA41.kampf.modifikator"}}</span>
<input type="number" name="modifikator" value="{{lookup formData "modifikator"}}">
<span>{{localize "DSA41.kampf.crit"}}</span>
<input class="center" type="checkbox" name="crit" {{checked (lookup formData "crit")}}>
<div class="dsa41-calculation colspan4 center">
{{#if formData.crit}}<ruby>2 * <rt>{{localize "DSA41.kampf.crit"}}</rt></ruby>({{/if}}
<ruby>{{options.item.item.system.fernkampfwaffe.basis}}<rt>{{localize "DSA41.bewaffnung.fernkampfwaffe.basis"}}</rt></ruby>
{{#if formData.crit}}){{/if}}
+ <ruby>{{lookup options.item.item.system.fernkampfwaffe formData.entfernung}}<rt>{{localize "DSA41.kampf.entfernung.label"}}</rt></ruby>
+ <ruby>{{formData.modifikator}}<rt>{{localize "DSA41.kampf.modifikator"}}</rt></ruby>
</div>
</div>
+34
View File
@@ -0,0 +1,34 @@
<div>
<span>{{localize "DSA41.kampf.modifikator"}}</span>
<input type="number" name="modifikator" value="{{lookup formData "modifikator"}}">
<span>{{localize "DSA41.kampf.crit"}}</span>
<input class="center" type="checkbox" name="crit" {{checked (lookup formData "crit")}}>
<div class="dsa41-calculation colspan4 center">
{{#if formData.crit}}({{/if}}
<ruby>{{options.item.basis_parade}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.basis"}}</rt></ruby>
+ <ruby>{{options.item.talent_parade}}<rt>{{localize "DSA41.talente.label"}}</rt></ruby>
{{#if (ne options.item.modifikator_parade 0)}}
+ <ruby>{{options.item.modifikator_parade}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.modifikator"}}</rt></ruby>
{{/if}}
{{#if (ne options.item.parierwaffe_parade 0)}}
+ <ruby>{{options.item.parierwaffe_parade}}<rt>{{localize "DSA41.bewaffnung.parierwaffe.label"}}</rt></ruby>
{{/if}}
{{#if (ne options.item.schild_parade 0)}}
+ <ruby>{{options.item.schild_parade}}<rt>{{localize "DSA41.bewaffnung.schild.label"}}</rt></ruby>
{{/if}}
{{#if (lt options.item.tp_kk 0)}}
+ <ruby>{{options.item.tp_kk}}<rt>{{localize "DSA41.attributes.long.strength"}}</rt></ruby>
{{/if}}
{{#if formData.crit}})<ruby> / 2<rt>{{localize "DSA41.kampf.crit"}}</rt></ruby>{{/if}}
+ <ruby>{{formData.modifikator}}<rt>{{localize "DSA41.kampf.modifikator"}}</rt></ruby>
</div>
</div>
+4
View File
@@ -0,0 +1,4 @@
<div>
<span class="colspan2">{{localize "DSA41.kampf.modifikator"}}</span>
<input class="colspan2" type="number" name="modifikator" value="{{lookup formData "modifikator"}}">
</div>
+19
View File
@@ -0,0 +1,19 @@
<div>
<span>{{localize "DSA41.kampf.modifikator"}}</span>
<input type="number" name="modifikator" value="{{lookup formData "modifikator"}}">
<span>{{localize "DSA41.kampf.crit"}}</span>
<input class="center" type="checkbox" name="crit" {{checked (lookup formData "crit")}}>
<div class="dsa41-calculation colspan4 center">
{{#if formData.crit}}<ruby>2 * <rt>{{localize "DSA41.kampf.crit"}}</rt></ruby>({{/if}}
<ruby>{{options.item.item.system.nahkampfwaffe.basis}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.basis"}}</rt></ruby>
{{#if formData.crit}}){{/if}}
{{#if (ne options.item.tp_kk 0)}}
+ <ruby>{{options.item.tp_kk}}<rt>{{localize "DSA41.attributes.long.strength"}}</rt></ruby>
{{/if}}
+ <ruby>{{formData.modifikator}}<rt>{{localize "DSA41.kampf.modifikator"}}</rt></ruby>
</div>
</div>
+12
View File
@@ -0,0 +1,12 @@
<div>
<select name="trefferzone" class="colspan4">
<option value="kopf" {{#if (eq formData.trefferzone "kopf") }}selected{{/if}}>{{localize "DSA41.ruestungen.kopf"}} </option>
<option value="brust" {{#if (eq formData.trefferzone "brust") }}selected{{/if}}>{{localize "DSA41.ruestungen.brust"}} </option>
<option value="ruecken" {{#if (eq formData.trefferzone "ruecken") }}selected{{/if}}>{{localize "DSA41.ruestungen.ruecken"}} </option>
<option value="bauch" {{#if (eq formData.trefferzone "bauch") }}selected{{/if}}>{{localize "DSA41.ruestungen.bauch"}} </option>
<option value="linker_arm" {{#if (eq formData.trefferzone "linker_arm") }}selected{{/if}}>{{localize "DSA41.ruestungen.linker_arm"}} </option>
<option value="rechter_arm" {{#if (eq formData.trefferzone "rechter_arm") }}selected{{/if}}>{{localize "DSA41.ruestungen.rechter_arm"}} </option>
<option value="linkes_bein" {{#if (eq formData.trefferzone "linkes_bein") }}selected{{/if}}>{{localize "DSA41.ruestungen.linkes_bein"}} </option>
<option value="rechtes_bein" {{#if (eq formData.trefferzone "rechtes_bein")}}selected{{/if}}>{{localize "DSA41.ruestungen.rechtes_bein"}}</option>
</select>
</div>
+9
View File
@@ -0,0 +1,9 @@
<div>
<span class="colspan2">{{localize "DSA41.kampf.modifikator"}}</span>
<input class="colspan2" type="number" name="modifikator" value="{{lookup formData "modifikator"}}">
{{#if options.item.system.magieresistenz}}
<span class="colspan2">{{localize "DSA41.zauber.label_magieresistenz"}}</span>
<input class="colspan2" type="number" name="magieresistenz" value="{{lookup formData "magieresistenz"}}">
{{/if}}
</div>
+11
View File
@@ -0,0 +1,11 @@
<div class="editable-input editable-{{type}} {{class}}">
{{#if (eq type "checkbox")}}
<input type="checkbox" name="{{name}}" {{#if data-name}}data-name="{{data-name}}"{{/if}} {{checked value}}>
{{else}}
<input type="{{type}}" name="{{name}}" {{#if data-name}}data-name="{{data-name}}"{{/if}} value="{{value}}" placeholder="{{placeholder}}">
{{/if}}
{{#if placeholder}}
<div class="placeholder">{{placeholder}}</div>
{{/if}}
</div>
-11
View File
@@ -1,11 +0,0 @@
<form class="item-sheet {{ cssClass }}" autocomplete="off">
<header>
{{#if editable}}
<img src="{{ item.img }}" title="{{ item.name }}" data-edit="img">
<input name="name" type="text" value="{{ item.name }}" placeholder="Name">
{{else}}
<img src="{{ item.img }}" title="{{ item.name }}">
<div>{{ item.name }}</div>
{{/if}}
</header>
</form>
+130
View File
@@ -0,0 +1,130 @@
<div class="Bewaffnung {{ cssClass }}">
<div class="row">
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" data-action="editImage" data-edit="img">
<div class="col">
{{DSA41_input "name" subtitle="DSA41.name"}}
<div class="grid2 gap">
{{DSA41_input "system.gewicht" subtitle="DSA41.weight.label"}}
{{DSA41_input "system.preis" subtitle="DSA41.price"}}
</div>
</div>
</div>
<nav class="tabs">
<div class="row noflex {{#if (eq tabGroups.primary 'tab1')}}active{{/if}}" data-group="primary" data-tab="tab1">
<a data-group="primary" data-tab="tab1" data-action="tab">{{localize "DSA41.bewaffnung.nahkampfwaffe.label"}}</a>
{{DSA41_input "system.nahkampfwaffe.aktiv"}}
</div>
<div class="row noflex {{#if (eq tabGroups.primary 'tab2')}}active{{/if}}" data-group="primary" data-tab="tab2">
<a data-group="primary" data-tab="tab2" data-action="tab">{{localize "DSA41.bewaffnung.parierwaffe.label"}}</a>
{{DSA41_input "system.parierwaffe.aktiv"}}
</div>
<div class="row noflex {{#if (eq tabGroups.primary 'tab3')}}active{{/if}}" data-group="primary" data-tab="tab3">
<a data-group="primary" data-tab="tab3" data-action="tab">{{localize "DSA41.bewaffnung.schild.label"}}</a>
{{DSA41_input "system.schild.aktiv"}}
</div>
<div class="row noflex {{#if (eq tabGroups.primary 'tab4')}}active{{/if}}" data-group="primary" data-tab="tab4">
<a data-group="primary" data-tab="tab4" data-action="tab">{{localize "DSA41.bewaffnung.fernkampfwaffe.label"}}</a>
{{DSA41_input "system.fernkampfwaffe.aktiv"}}
</div>
</nav>
<div class="tab {{#if (eq tabGroups.primary 'tab1')}}active{{/if}}" data-group="primary" data-tab="tab1">
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.trefferpunkte"}}</span>
<div class="grid3 center">
{{DSA41_input "system.nahkampfwaffe.basis" subtitle="DSA41.bewaffnung.nahkampfwaffe.basis"}}
{{DSA41_input "system.nahkampfwaffe.schwellenwert" subtitle="DSA41.bewaffnung.nahkampfwaffe.schwellenwert"}}
{{DSA41_input "system.nahkampfwaffe.schadensschritte" subtitle="DSA41.bewaffnung.nahkampfwaffe.schadensschritte"}}
</div>
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.modifikator"}}</span>
<div class="grid2">
{{DSA41_input "system.nahkampfwaffe.modifikator_attacke" subtitle="DSA41.bewaffnung.nahkampfwaffe.attacke"}}
{{DSA41_input "system.nahkampfwaffe.modifikator_parade" subtitle="DSA41.bewaffnung.nahkampfwaffe.parade"}}
</div>
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.initiative"}}</span>
<div>{{DSA41_input "system.nahkampfwaffe.initiative"}}</div>
<span>{{localize "DSA41.bewaffnung.bruchfaktor"}}</span>
<div>{{DSA41_input "system.nahkampfwaffe.bruchfaktor"}}</div>
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.distanzklasse"}}</span>
<div>{{DSA41_input "system.nahkampfwaffe.distanzklasse"}}</div>
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.kampftalente"}}</span>
<div>{{DSA41_input "system.nahkampfwaffe.kampftalente"}}</div>
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.laenge"}}</span>
<div>{{DSA41_input "system.nahkampfwaffe.laenge"}}</div>
<div class="colspan2"></div>
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.zweihaendig"}}</span>
<div>{{DSA41_input "system.nahkampfwaffe.zweihaendig"}}</div>
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.werfbar"}}</span>
<div>{{DSA41_input "system.nahkampfwaffe.werfbar"}}</div>
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.improvisiert"}}</span>
<div>{{DSA41_input "system.nahkampfwaffe.improvisiert"}}</div>
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.priviligiert"}}</span>
<div>{{DSA41_input "system.nahkampfwaffe.priviligiert"}}</div>
</div>
<div class="tab {{#if (eq tabGroups.primary 'tab2')}}active{{/if}}" data-group="primary" data-tab="tab2">
<span>{{localize "DSA41.bewaffnung.parierwaffe.initiative"}}</span>
<div>{{DSA41_input "system.parierwaffe.initiative"}}</div>
<span>{{localize "DSA41.bewaffnung.parierwaffe.modifikator"}}</span>
<div class="grid2">
{{DSA41_input "system.parierwaffe.modifikator_attacke" subtitle="DSA41.bewaffnung.parierwaffe.attacke"}}
{{DSA41_input "system.parierwaffe.modifikator_parade" subtitle="DSA41.bewaffnung.parierwaffe.parade"}}
</div>
<span>{{localize "DSA41.bewaffnung.bruchfaktor"}}</span>
<div>{{DSA41_input "system.parierwaffe.bruchfaktor"}}</div>
</div>
<div class="tab {{#if (eq tabGroups.primary 'tab3')}}active{{/if}}" data-group="primary" data-tab="tab3">
<span>{{localize "DSA41.bewaffnung.schild.groesse.label"}}</span>
{{DSA41_input "system.schild.groesse"}}
<span>{{localize "DSA41.bewaffnung.schild.modifikator"}}</span>
<div class="grid2">
{{DSA41_input "system.schild.modifikator_attacke" subtitle="DSA41.bewaffnung.schild.attacke"}}
{{DSA41_input "system.schild.modifikator_parade" subtitle="DSA41.bewaffnung.schild.parade"}}
</div>
<span>{{localize "DSA41.bewaffnung.schild.initiative"}}</span>
<div>{{DSA41_input "system.schild.initiative"}}</div>
<span>{{localize "DSA41.bewaffnung.bruchfaktor"}}</span>
<div>{{DSA41_input "system.schild.bruchfaktor"}}</div>
</div>
<div class="tab {{#if (eq tabGroups.primary 'tab4')}}active{{/if}}" data-group="primary" data-tab="tab4">
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.trefferpunkte"}}</span>
<div class="center">{{DSA41_input "system.fernkampfwaffe.basis"}}</div>
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.laden"}}</span>
<div>{{DSA41_input "system.fernkampfwaffe.laden"}}</div>
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.munitionskosten"}}</span>
<div>{{DSA41_input "system.fernkampfwaffe.munitionskosten"}}</div>
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.munitionsgewicht"}}</span>
<div>{{DSA41_input "system.fernkampfwaffe.munitionsgewicht"}}</div>
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.reichweiten"}}</span>
<div class="colspan3 grid5">
{{DSA41_input "system.fernkampfwaffe.reichweite1"}}
{{DSA41_input "system.fernkampfwaffe.reichweite2"}}
{{DSA41_input "system.fernkampfwaffe.reichweite3"}}
{{DSA41_input "system.fernkampfwaffe.reichweite4"}}
{{DSA41_input "system.fernkampfwaffe.reichweite5"}}
</div>
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.modifikator"}}</span>
<div class="row colspan3 grid5">
{{DSA41_input "system.fernkampfwaffe.modifikator1"}}
{{DSA41_input "system.fernkampfwaffe.modifikator2"}}
{{DSA41_input "system.fernkampfwaffe.modifikator3"}}
{{DSA41_input "system.fernkampfwaffe.modifikator4"}}
{{DSA41_input "system.fernkampfwaffe.modifikator5"}}
</div>
</div>
<div>
{{DSA41_input "system.beschreibung" elementType="prose-mirror"}}
</div>
</div>
+16
View File
@@ -0,0 +1,16 @@
<div class="item-sheet {{ cssClass }}" autocomplete="off">
<div class="row">
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" data-action="editImage" data-edit="img">
<div class="col">
{{DSA41_input "name" subtitle="DSA41.name"}}
<div class="grid2 gap">
{{DSA41_input "system.gewicht" subtitle="DSA41.weight.label"}}
{{DSA41_input "system.preis" subtitle="DSA41.price"}}
</div>
</div>
</div>
<div>
{{DSA41_input "system.beschreibung" elementType="prose-mirror"}}
</div>
</div>
-11
View File
@@ -1,11 +0,0 @@
<form class="item-sheet {{ cssClass }}" autocomplete="off">
<header>
{{#if editable}}
<img src="{{ item.img }}" title="{{ item.name }}" data-edit="img" >
<input name="name" type="text" value="{{ item.name }}" placeholder="Name">
{{else}}
<img src="{{ item.img }}" title="{{ item.name }}">
<div>{{ item.name }}</div>
{{/if}}
</header>
</form>
+18
View File
@@ -0,0 +1,18 @@
<div class="item-sheet {{ cssClass }}" autocomplete="off">
<div class="row">
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" data-action="editImage" data-edit="img">
<div class="col">
<div class="grid3 gap">
{{DSA41_input "name" subtitle="DSA41.name"}}
{{DSA41_input "system.steigern" subtitle="DSA41.kampftalent.label_steigern"}}
{{DSA41_input "system.behinderung" subtitle="DSA41.talente.label_behinderung"}}
</div>
<div class="grid gap">
{{DSA41_input "system.kategorie" subtitle="DSA41.talente.label_kategorie"}}
</div>
</div>
</div>
<div>
{{DSA41_input "system.beschreibung" elementType="prose-mirror"}}
</div>
</div>
-11
View File
@@ -1,11 +0,0 @@
<form class="item-sheet {{ cssClass }}" autocomplete="off">
<header>
{{#if editable}}
<img src="{{ item.img }}" title="{{ item.name }}" data-edit="img" >
<input name="name" type="text" value="{{ item.name }}" placeholder="Name">
{{else}}
<img src="{{ item.img }}" title="{{ item.name }}">
<div>{{ item.name }}</div>
{{/if}}
</header>
</form>
-11
View File
@@ -1,11 +0,0 @@
<form class="item-sheet {{ cssClass }}" autocomplete="off">
<header>
{{#if editable}}
<img src="{{ item.img }}" title="{{ item.name }}" data-edit="img" >
<input name="name" type="text" value="{{ item.name }}" placeholder="Name">
{{else}}
<img src="{{ item.img }}" title="{{ item.name }}">
<div>{{ item.name }}</div>
{{/if}}
</header>
</form>
+43
View File
@@ -0,0 +1,43 @@
<div class="item-sheet {{ cssClass }}" autocomplete="off">
<div class="row">
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" data-action="editImage" data-edit="img">
<div class="col">
{{DSA41_input "name" subtitle="DSA41.name"}}
<div class="grid2 gap">
{{DSA41_input "system.gewicht" subtitle="DSA41.weight.label"}}
{{DSA41_input "system.preis" subtitle="DSA41.price"}}
</div>
</div>
</div>
<div class="tab active">
<span>{{localize "DSA41.ruestungen.kopf"}}</span>
<div>{{DSA41_input "system.kopf"}}</div>
<span>{{localize "DSA41.ruestungen.brust"}}</span>
<div>{{DSA41_input "system.brust"}}</div>
<span>{{localize "DSA41.ruestungen.ruecken"}}</span>
<div>{{DSA41_input "system.ruecken"}}</div>
<span>{{localize "DSA41.ruestungen.bauch"}}</span>
<div>{{DSA41_input "system.bauch"}}</div>
<span>{{localize "DSA41.ruestungen.linker_arm"}}</span>
<div>{{DSA41_input "system.linker_arm"}}</div>
<span>{{localize "DSA41.ruestungen.rechter_arm"}}</span>
<div>{{DSA41_input "system.rechter_arm"}}</div>
<span>{{localize "DSA41.ruestungen.linkes_bein"}}</span>
<div>{{DSA41_input "system.linkes_bein"}}</div>
<span>{{localize "DSA41.ruestungen.rechtes_bein"}}</span>
<div>{{DSA41_input "system.rechtes_bein"}}</div>
<span>{{localize "DSA41.ruestungen.gesamt_ruestungsschutz"}}</span>
<div>{{DSA41_input "system.gesamt_ruestungsschutz"}}</div>
<span>{{localize "DSA41.ruestungen.gesamt_behinderung"}}</span>
<div>{{DSA41_input "system.gesamt_behinderung"}}</div>
</div>
<div>
{{DSA41_input "system.beschreibung" elementType="prose-mirror"}}
</div>
</div>
-11
View File
@@ -1,11 +0,0 @@
<form class="item-sheet {{ cssClass }}" autocomplete="off">
<header>
{{#if editable}}
<img src="{{ item.img }}" title="{{ item.name }}" data-edit="img">
<input name="name" type="text" value="{{ item.name }}" placeholder="Name">
{{else}}
<img src="{{ item.img }}" title="{{ item.name }}">
<div>{{ item.name }}</div>
{{/if}}
</header>
</form>
+18
View File
@@ -0,0 +1,18 @@
<div class="item-sheet {{ cssClass }}" autocomplete="off">
<div class="row">
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" data-action="editImage" data-edit="img">
<div class="col">
<div class="grid2 gap">
{{DSA41_input "name" subtitle="DSA41.name"}}
{{DSA41_input "system.kategorie" subtitle="DSA41.sonderfertigkeiten.kategorie.label"}}
</div>
<div class="grid2 gap">
{{DSA41_input "system.kosten" subtitle="DSA41.sonderfertigkeiten.kosten"}}
{{DSA41_input "system.verbreitung" subtitle="DSA41.sonderfertigkeiten.verbreitung"}}
</div>
</div>
</div>
<div>
{{DSA41_input "system.beschreibung" elementType="prose-mirror"}}
</div>
</div>
+20
View File
@@ -0,0 +1,20 @@
<div class="item-sheet {{ cssClass }}" autocomplete="off">
<div class="row">
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" data-action="editImage" data-edit="img">
<div class="col">
<div class="grid3 gap">
{{DSA41_input "name" subtitle="DSA41.name"}}
{{DSA41_input "system.kategorie" subtitle="DSA41.talente.label_kategorie"}}
{{DSA41_input "system.behinderung" subtitle="DSA41.talente.label_behinderung"}}
</div>
<div class="grid3 gap">
{{DSA41_input "system.attribute1"}}
{{DSA41_input "system.attribute2"}}
{{DSA41_input "system.attribute3"}}
</div>
</div>
</div>
<div>
{{DSA41_input "system.beschreibung" elementType="prose-mirror"}}
</div>
</div>
+17
View File
@@ -0,0 +1,17 @@
<div class="item-sheet {{ cssClass }}" autocomplete="off">
<div class="row">
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" data-action="editImage" data-edit="img">
<div class="col">
<div class="grid2 gap">
{{DSA41_input "name" subtitle="DSA41.name"}}
{{DSA41_input "system.kosten" subtitle="DSA41.vornachteil.kosten"}}
</div>
<div class="grid gap">
{{DSA41_input "system.kategorie" subtitle="DSA41.vornachteil.kategorie.label"}}
</div>
</div>
</div>
<div>
{{DSA41_input "system.beschreibung" elementType="prose-mirror"}}
</div>
</div>
+35
View File
@@ -0,0 +1,35 @@
<div class="item-sheet {{ cssClass }}" autocomplete="off">
<div class="row">
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" data-action="editImage" data-edit="img">
<div class="col">
<div class="grid3 gap">
{{DSA41_input "name"}}
{{DSA41_input "system.repraesentation" subtitle="DSA41.zauber.repraesentation.label"}}
{{DSA41_input "system.komplexitaet" subtitle="DSA41.zauber.label_komplexitaet"}}
</div>
<div class="grid3 gap">
{{DSA41_input "system.attribute1"}}
{{DSA41_input "system.attribute2"}}
{{DSA41_input "system.attribute3"}}
</div>
</div>
</div>
<div class="tab active">
<span>{{localize "DSA41.zauber.label_zauberdauer"}}</span>
{{DSA41_input "system.zauberdauer"}}
<span>{{localize "DSA41.zauber.label_wirkungsdauer"}}</span>
{{DSA41_input "system.wirkungsdauer"}}
<span>{{localize "DSA41.zauber.merkmale.label"}}</span>
{{DSA41_input "system.merkmale"}}
<span>{{localize "DSA41.zauber.label_magieresistenz"}}</span>
{{DSA41_input "system.magieresistenz"}}
</div>
<div>
{{DSA41_input "system.beschreibung" elementType="prose-mirror"}}
</div>
</div>
+21
View File
@@ -0,0 +1,21 @@
<div class="dsa41-tooltip">
<ruby>{{basis_attacke}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.basis"}}</rt></ruby>
+ <ruby>{{talent_attacke}}<rt>{{localize "DSA41.talente.label"}}</rt></ruby>
{{#if (ne modifikator_attacke 0)}}
+ <ruby>{{modifikator_attacke}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.modifikator"}}</rt></ruby>
{{/if}}
{{#if (ne parierwaffe_attacke 0)}}
+ <ruby>{{parierwaffe_attacke}}<rt>{{localize "DSA41.bewaffnung.parierwaffe.label"}}</rt></ruby>
{{/if}}
{{#if (ne schild_attacke 0)}}
+ <ruby>{{schild_attacke}}<rt>{{localize "DSA41.bewaffnung.schild.label"}}</rt></ruby>
{{/if}}
{{#if (lt tp_kk 0)}}
+ <ruby>{{tp_kk}}<rt>{{localize "DSA41.attributes.long.strength"}}</rt></ruby>
{{/if}}
</div>
+9
View File
@@ -0,0 +1,9 @@
<div class="dsa41-tooltip">
<ruby>{{initial}}<rt>{{localize "DSA41.attributes.initial"}}</rt></ruby>
{{#if (ne advancement 0)}}
+ <ruby>{{advancement}}<rt>{{localize "DSA41.attributes.advancement"}}</rt></ruby>
{{/if}}
{{#if (ne modifier 0)}}
+ <ruby>{{modifier}}<rt>{{localize "DSA41.attributes.modifier"}}</rt></ruby>
{{/if}}
</div>
+4
View File
@@ -0,0 +1,4 @@
<div class="dsa41-tooltip">
<ruby>{{basis_attacke}}<rt>{{localize "DSA41.bewaffnung.fernkampfwaffe.basis"}}</rt></ruby>
+ <ruby>{{talent_attacke}}<rt>{{localize "DSA41.talente.label"}}</rt></ruby>
</div>
+3
View File
@@ -0,0 +1,3 @@
<div class="dsa41-tooltip">
<ruby>{{item.system.fernkampfwaffe.basis}}<rt>{{localize "DSA41.bewaffnung.fernkampfwaffe.basis"}}</rt></ruby>
</div>
+21
View File
@@ -0,0 +1,21 @@
<div class="dsa41-tooltip">
<ruby>{{basis_parade}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.basis"}}</rt></ruby>
+ <ruby>{{talent_parade}}<rt>{{localize "DSA41.talente.label"}}</rt></ruby>
{{#if (ne modifikator_parade 0)}}
+ <ruby>{{modifikator_parade}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.modifikator"}}</rt></ruby>
{{/if}}
{{#if (ne parierwaffe_parade 0)}}
+ <ruby>{{parierwaffe_parade}}<rt>{{localize "DSA41.bewaffnung.parierwaffe.label"}}</rt></ruby>
{{/if}}
{{#if (ne schild_parade 0)}}
+ <ruby>{{schild_parade}}<rt>{{localize "DSA41.bewaffnung.schild.label"}}</rt></ruby>
{{/if}}
{{#if (lt tp_kk 0)}}
+ <ruby>{{tp_kk}}<rt>{{localize "DSA41.attributes.long.strength"}}</rt></ruby>
{{/if}}
</div>
+7
View File
@@ -0,0 +1,7 @@
<div class="dsa41-tooltip">
<ruby>{{item.system.nahkampfwaffe.basis}}<rt>{{localize "DSA41.bewaffnung.nahkampfwaffe.basis"}}</rt></ruby>
{{#if (ne tp_kk 0)}}
+ <ruby>{{tp_kk}}<rt>{{localize "DSA41.attributes.long.strength"}}</rt></ruby>
{{/if}}
</div>
+590 -5
View File
@@ -1,9 +1,186 @@
{
"TYPES": {
"Actor": {
"Player": "Spieler"
},
"Item": {
"Gegenstand": "Gegenstand",
"Ruestung": "Rüstung",
"Bewaffnung": "Bewaffnung",
"Talent": "Talent",
"Kampftalent": "Kampftalent",
"Sonderfertigkeit": "Sonderfertigkeit",
"VorNachteil": "Vor-/Nachteil",
"Zauber": "Zauber"
}
},
"DSA41": {
"name": "Name",
"race": "Rasse",
"culture": "Kultur",
"profession": "Profession",
"name": "Name",
"race": "Rasse",
"culture": "Kultur",
"profession": "Profession",
"sozialstatus": "Sozialstatus",
"price": "Preis",
"abenteuerpunkte": "Abenteuerpunkte",
"list_empty": "Keine",
"currency": {
"dukaten": "Dukaten",
"silbertaler": "Silbertaler",
"heller": "Heller",
"kreuzer": "Kreuzer"
},
"weight": {
"label": "Gewicht",
"gran": "Gran",
"karat": "Karat",
"skrupel": "Skrupel",
"unze": "Unzen",
"stein": "Steine",
"sack": "Säcke",
"quader": "Quader"
},
"laenge": {
"halbfinger": "Halbfinger",
"finger": "Finger",
"spann": "Spann",
"schritt": "Schritt",
"faden": "Faden",
"lot": "Lot",
"meile": "Meile",
"tagesreise": "Tagesreise",
"baryd": "Baryd"
},
"steigerungskategorie": {
"A_Star": "A*",
"A": "A",
"B": "B",
"C": "C",
"D": "D",
"E": "E",
"F": "F",
"G": "G",
"H": "H"
},
"chat": {
"result": "Ergebnis",
"success": "Geschafft",
"failure": "Fehlgeschlagen",
"attribute": "Eigenshaft",
"value": "Wert",
"roll": "Wurf",
"talentwert_short": "TaW",
"zauberfertigkeitswert_short": "ZfW",
"targets": "Ziele",
"trefferpunkte_apply": "Zuweisen"
},
"basiswerte": {
"label_basiswert": "Basiswert",
"label_modifikator": "Modifikator",
"label_zukauf": "Zukauf",
"label_verlust": "Verlust",
"label_total": "Total",
"lebenspunkte": "Lebenspunkte",
"ausdauer": "Ausdauer",
"astralenergie": "Astralenergie",
"karmalenergie": "Karmalenergie",
"magieresistenz": "Magieresistenz",
"initiative": "Initiative",
"attacke": "Attacke",
"parade": "Parade",
"fernkampf": "Fernkampf"
},
"vornachteil": {
"label_vorteile": "Vorteile",
"label_nachteile": "Nachteile",
"kosten": "Kosten",
"kategorie": {
"label": "Kategorie",
"vorteil": "Vorteil",
"nachteil": "Nachteil"
}
},
"sonderfertigkeiten": {
"label_allgemein": "Allgemeine Sonderfertigkeiten",
"label_kampf": "Kampf-Sonderfertigkeiten",
"label_magisch": "Magische Sonderfertigkeiten",
"label_klerikal": "Klerikale Sonderfertigkeiten",
"kosten": "Kosten",
"verbreitung": "Verbreitung",
"kategorie": {
"label": "Kategorie",
"allgemein": "Allgemein",
"kampf": "Kampf",
"magisch": "Magisch",
"klerikal": "Klerikal"
}
},
"roll_types": {
"courage": "Mut",
"cleverness": "Klugheit",
"intuition": "Intuition",
"charisma": "Charisma",
"dexterity": "Fingerfertigkeit",
"agility": "Gewandheit",
"constitution": "Konstitution",
"strength": "Körperkraft",
"talent": "Talent",
"zauber": "Zauber",
"attacke": "Attacke",
"parade": "Parade",
"trefferpunkte": "Trefferpunkte",
"fernkampf-attacke": "Attacke",
"fernkampf-trefferpunkte": "Trefferpunkte"
},
"character": {
"eigenschaften": "Eigenschaften",
"talente": "Talente",
"inventar": "Inventar",
"kampf": "Kampf",
"allgemein": "Allgemein",
"zauber": "Zauber"
},
"allgemein": {
"geschlecht": "Geschlecht",
"alter": "Alter",
"groesse": "Größe",
"gewicht": "Gewicht",
"haarfarbe": "Haarfarbe",
"augenfarbe": "Augenfarbe",
"stand": "Stand",
"titel": "Titel",
"aussehen": "Aussehen",
"hintergrund": "Familie/Herkunft/Hintergrund",
"biografie": "Biografie"
},
"attributes": {
"label": "Eigenschaften",
@@ -32,6 +209,414 @@
"constitution": "KO",
"strength": "KK"
}
},
"kampftalent": {
"label": "Kampftalent",
"label_kategorie": "Kategorie",
"label_steigern": "Steigern",
"kategorie":{
"waffenlos": "Waffenlos",
"nahkampf": "Bewaffneter Nahkampf",
"fernkampf": "Fernkampf"
}
},
"talente":{
"label": "Talent",
"label_eigenschaften": "Eigenschaften",
"label_talentwert": "Talentwert",
"label_kategorie": "Kategorie",
"label_behinderung": "Behinderung",
"kampf": {
"label": "Kampf Talente",
"label_attacke": "Attacke",
"label_parade": "Parade",
"label_attacke_total": "Attacke",
"label_parade_total": "Parade"
},
"koerperliche": {
"label": "Körperliche Talente",
"name": {
"Akrobatik": "Akrobatik",
"Athletik": "Athletik",
"Fliegen": "Fliegen",
"Gaukeleien": "Gaukeleien",
"Klettern": "Klettern",
"Körperbeherrschung": "Körperbeherrschung",
"Reiten": "Reiten",
"Schleichen": "Schleichen",
"Schwimmen": "Schwimmen",
"Selbstbeherrschung": "Selbstbeherrschung",
"Sich Verstecken": "Sich Verstecken",
"Singen": "Singen",
"Sinnenschärfe": "Sinnenschärfe",
"Skifahren": "Skifahren",
"Stimmen Imitieren": "Stimmen Imitieren",
"Tanzen": "Tanzen",
"Taschendiebstahl": "Taschendiebstahl",
"Zechen": "Zechen"
}
},
"gesellschaftliche": {
"label": "Gesellschaftliche Talente",
"name": {
"Betören": "Betören",
"Etikette": "Etikette",
"Gassenwissen": "Gassenwissen",
"Lehren": "Lehren",
"Menschenkenntnis": "Menschenkenntnis",
"Schauspielerei": "Schauspielerei",
"Schriftlicher Ausdruck": "Schriftlicher Ausdruck",
"Sich Verkleiden": "Sich Verkleiden",
"Überreden": "Überreden",
"Überzeugen": "Überzeugen"
}
},
"natur": {
"label": "Natur-Talente",
"name": {
"Fährtensuchen": "Fährtensuchen",
"Fallenstellen": "Fallenstellen",
"Fesseln/Entfesseln": "Fesseln/Entfesseln",
"Fischen/Angeln": "Fischen/Angeln",
"Orientierung": "Orientierung",
"Wettervorhersage": "Wettervorhersage",
"Wildnisleben": "Wildnisleben"
}
},
"wissens": {
"label": "Wissenstalente",
"name": {
"Anatomie": "Anatomie",
"Baukunst": "Baukunst",
"Brett-/Kartenspiel": "Brett-/Kartenspiel",
"Geographie": "Geographie",
"Geschichtswissen": "Geschichtswissen",
"Gesteinskunde": "Gesteinskunde",
"Götter/Kulte": "Götter/Kulte",
"Heraldik": "Heraldik",
"Hüttenkunde": "Hüttenkunde",
"Kriegskunst": "Kriegskunst",
"Kryptographie": "Kryptographie",
"Magiekunde": "Magiekunde",
"Mechanik": "Mechanik",
"Pflanzenkunde": "Pflanzenkunde",
"Philosophie": "Philosophie",
"Rechnen": "Rechnen",
"Rechtskunde": "Rechtskunde",
"Sagen/Legenden": "Sagen/Legenden",
"Schätzen": "Schätzen",
"Sprachenkunde": "Sprachenkunde",
"Staatskunst": "Staatskunst",
"Sternenkunde": "Sternenkunde",
"Tierkunde": "Tierkunde"
}
},
"sprachen": {
"label": "Sprachen und Schriften",
"name": {
"lesen_schreiben": "Lesen/Schreiben [Schrift]",
"muttersprache": "Sprachen [Muttersprache]",
"fremdsprache": "Sprachen [Fremdsprache]"
}
},
"handwerks": {
"label": "Handwerkstalente",
"name": {
"Abrichten": "Abrichten",
"Ackerbau": "Ackerbau",
"Alchimie": "Alchimie",
"Bergbau": "Bergbau",
"Bogenbau": "Bogenbau",
"Boote Fahren": "Boote Fahren",
"Brauer": "Brauer",
"Drucker": "Drucker",
"Fahrzeug Lenken": "Fahrzeug Lenken",
"Falschspiel": "Falschspiel",
"Feinmechanik": "Feinmechanik",
"Feuersteinbearbeitung": "Feuersteinbearbeitung",
"Fleischer": "Fleischer",
"Gerber/Kürschner": "Gerber/Kürschner",
"Glaskunst": "Glaskunst",
"Grobschmied": "Grobschmied",
"Handel": "Handel",
"Hauswirtschaft": "Hauswirtschaft",
"Heilkunde Gift": "Heilkunde Gift",
"Heilkunde Krankheiten": "Heilkunde Krankheiten",
"Heilkunde Seele": "Heilkunde Seele",
"Heilkunde Wunden": "Heilkunde Wunden",
"Holzbearbeitung": "Holzbearbeitung",
"Instrumentenbauer": "Instrumentenbauer",
"Kartographie": "Kartographie",
"Kochen": "Kochen",
"Kristallzucht": "Kristallzucht",
"Lederarbeiten": "Lederarbeiten",
"Malen/Zeichnen": "Malen/Zeichnen",
"Maurer": "Maurer",
"Metallguss": "Metallguss",
"Musizieren": "Musizieren",
"Schlösser Knacken": "Schlösser Knacken",
"Schnapps Brennen": "Schnapps Brennen",
"Schneidern": "Schneidern",
"Seefahrt": "Seefahrt",
"Seiler": "Seiler",
"Steinmetz": "Steinmetz",
"Steinschneider/Juwelier": "Steinschneider/Juwelier",
"Stellmacher": "Stellmacher",
"Stoffe Faerben": "Stoffe Faerben",
"Tätowieren": "Tätowieren",
"Töpfern": "Töpfern",
"Viehzucht": "Viehzucht",
"Webkunst": "Webkunst",
"Winzer": "Winzer",
"Zimmermann": "Zimmermann"
}
}
},
"ruestungen": {
"kopf": "Kopf",
"brust": "Brust",
"ruecken": "Rücken",
"bauch": "Bauch",
"linker_arm": "Linker Arm",
"rechter_arm": "Rechter Arm",
"linkes_bein": "Linkes Bein",
"rechtes_bein": "Rechtes Bein",
"gesamt_ruestungsschutz": "Gesamt Rüstungsschutz",
"gesamt_behinderung": "Gesamt Behinderung",
"arm": "Arm",
"bein": "Bein",
"ruestungsschutz": "Rüstungsschutz",
"behinderung": "Behinderung",
"gesamt": "Gesamt",
"links": "Links",
"rechts": "Rechts"
},
"bewaffnung": {
"bruchfaktor": "Bruchfaktor",
"nahkampfwaffe": {
"label": "Nahkampfwaffe",
"laenge": "Länge",
"trefferpunkte": "Trefferpunkte",
"basis": "Basis",
"schwellenwert": "Schwellenwert",
"schadensschritte": "Schadensschritte",
"initiative": "Initiative",
"modifikator": "Modifikator",
"attacke": "Attacke",
"parade": "Parade",
"distanzklasse": "Distanzklasse",
"zweihaendig": "Zweihändig",
"werfbar": "Werfbar",
"improvisiert": "Improvisiert",
"priviligiert": "Priviligiert",
"kampftalente": "Kampftalente"
},
"parierwaffe": {
"label": "Parierwaffe",
"modifikator": "Modifikator",
"attacke": "Attacke",
"parade": "Parade",
"initiative": "Initiative"
},
"schild": {
"label": "Schild",
"groesse": {
"label": "Größe",
"klein": "Klein",
"gross": "Groß",
"sehr_gross": "Sehr Groß"
},
"modifikator": "Modifikator",
"attacke": "Attacke",
"parade": "Parade",
"initiative": "Initiative"
},
"fernkampfwaffe": {
"label": "Fernkampfwaffe",
"trefferpunkte": "Trefferpunkte",
"basis": "Basis",
"reichweiten": "Reichweiten",
"modifikator": "Modifikator",
"laden": "Laden",
"munitionskosten": "Munitionskosten",
"munitionsgewicht": "Munitionsgewicht"
}
},
"inventar": {
"bewaffnung": "Bewaffnung",
"ruestungen": "Rüstungen",
"gegenstaende": "Gegenstände"
},
"kampf": {
"bewaffnung": "Bewaffnung",
"attacke": "Attacke",
"parade": "Parade",
"trefferpunkte": "Trefferpunkte",
"ruestungen": "Rüstungen",
"modifikator": "Modifikator",
"crit": "Glückliche Attacke",
"zauber": "Zauber",
"zielgroesse": {
"label": "Zielgröße",
"winzig": "Winzig",
"sehr_klein": "Sehr Klein",
"klein": "Klein",
"mittel": "Mittel",
"gross": "Groß",
"sehr_gross": "Sehr Groß"
},
"deckung": {
"label": "Deckung",
"keine": "Keine",
"halb": "Halb",
"drei_viertel": "Drei Viertel"
},
"entfernung": {
"label": "Entfernung",
"sehr_nah": "Sehr Nah",
"nah": "Nah",
"mittel": "Mittel",
"weit": "Weit",
"extrem_weit": "Extrem Weit"
},
"zielbewegung": {
"label": "Bewegung des Ziels",
"unbeweglich": "Unbeweglich",
"stillstehend": "Stillstehend",
"leicht": "Leicht",
"schnell": "Schnell",
"sehr_schnell": "Sehr Schnell"
},
"wind": {
"label": "Wind",
"still": "Still",
"seitenwind": "Böiger Seitenwind",
"starker_seitenwind": "Starker Seitenwind"
}
},
"zauber": {
"label_komplexitaet": "Komplexität",
"label_zauberdauer": "Zauberdauer",
"label_wirkungsdauer": "Wirkungsdauer",
"label_zauberfertigkeitswert": "Zauberfertigkeitswert",
"label_magieresistenz": "Magieresistenz",
"merkmale": {
"label": "Merkmale",
"anitmagie": "Antimagie",
"beschwoerung": "Beschwörung",
"daemonisch_allgemein": "Dämonisch (Allgemein)",
"daemonisch_agrimoth_widharcal": "Dämonisch (Agrimoth / Widharcal)",
"daemonisch_amazeroth_iribaar": "Dämonisch (Amazeroth / Iribaar)",
"daemonisch_asfaloth_calijnaar": "Dämonisch (Asfaloth / Calijnaar)",
"daemonisch_belhalhar_xarfai": "Dämonisch (Belhalhar / Xarfai)",
"daemonisch_blakharaz_tyakraman": "Dämonisch (Blakharaz / Tyakraman)",
"daemonisch_lolgramoth_thezzphai": "Dämonisch (Lolgramoth / Thezzphai)",
"daemonisch_belzhorash_mishkara": "Dämonisch (Belzhorash / Mishkara)",
"daemonisch_thargunitoth_tijakool": "Dämonisch (Thargunitoth / Tijakool)",
"eigenschaften": "Eigenschaften",
"einfluss": "Einfluss",
"elementar_allgemein": "Elementar (Allgemein)",
"elementar_eis": "Elementar (Eis)",
"elementar_erz": "Elementar (Erz)",
"elementar_feuer": "Elementar (Feuer)",
"elementar_humus": "Elementar (Humus)",
"elementar_luft": "Elementar (Luft)",
"elementar_wasser": "Elementar (Wasser)",
"form": "Form",
"geisterwesen": "Geisterwesen",
"heilung": "Heilung",
"hellsicht": "Hellsicht",
"herbeirufung": "Herbeirufung",
"herrschaft": "Herrschaft",
"illusion": "Illusion",
"kraft": "Kraft",
"limbus": "Limbus",
"metamagie": "Metamagie",
"objekt": "Objekt",
"schaden": "Schaden",
"telekinese": "Telekinese",
"temporal": "Temporal",
"umwelt": "Umwelt",
"verständigung": "Verständigung"
},
"repraesentation": {
"label": "Repräsentation",
"borbaradianisch": "Borbaradianisch",
"druidisch": "Druidisch",
"elfisch": "Elfisch",
"geodisch": "Geodisch",
"satuarisch": "Satuarisch",
"kristallomantisch": "Kristallomantisch",
"gildenmagisch": "Gildenmagisch",
"scharlatanisch": "Scharlatanisch",
"schelmisch": "Schelmisch"
},
"modifikationen": {
"zauberdauer": "Zauberdauer",
"erzwingen": "Erzwingen",
"kosten": "Kosten",
"mehrere_ziele": "Mehrere Ziele",
"mehrere_ziele_freiwillig": "Mehrere Ziele (Freiwillig)",
"reichweite": "Reichweite",
"reichweite_beruehrung": "Reichweite (Berührung)",
"reichweite_selbst": "Reichweite (Selbst)",
"wirkungsdauer": "Wirkungsdauer",
"ziel_objekt_einzeln": "Zielobjekt (Einzeln)",
"ziel_objekt_freiwillig": "Zielobjekt (Freiwillig)",
"ziel_objekt_unfreiwillig": "Zielobjekt (Unfreiwillig)",
"ziel_objekt_mehrere": "Zielobjekt (Mehrere)"
}
}
}
}
}
+189
View File
@@ -1,9 +1,19 @@
{
"TYPES": {
"Item": {
"Gegenstand": "Generic",
"Ruestung": "Armor",
"Bewaffnung": "Weaponry"
}
},
"DSA41": {
"name": "Name",
"race": "Race",
"culture": "Culture",
"profession": "Profession",
"weight": "Weight",
"price": "Price",
"attributes": {
"label": "Attributes",
@@ -32,6 +42,185 @@
"constitution": "CN",
"strength": "ST"
}
},
"talente": {
"label": "Talent",
"label_eigenschaften": "Attributes",
"label_talentwert": "Talent Prowess",
"kampf": {
"label": "Combat Talents",
"label_attacke": "!!TODO!!",
"label_parade": "!!TODO!!",
"label_attacke_total": "!!TODO!!",
"label_parade_total": "!!TODO!!",
"name": {
"anderthalbhaender": "!!TODO!!",
"armbrust": "!!TODO!!"
}
},
"koerperliche": {
"label": "!!TODO!!",
"name": {
"akrobatik": "!!TODO!!",
"athletik": "!!TODO!!",
"fliegen": "!!TODO!!",
"gaukeleien": "!!TODO!!",
"klettern": "!!TODO!!",
"koerperbeherrschung": "!!TODO!!",
"reiten": "!!TODO!!",
"schleichen": "!!TODO!!",
"schwimmen": "!!TODO!!",
"selbstbeherrschung": "!!TODO!!",
"sich_verstecken": "!!TODO!!",
"singen": "!!TODO!!",
"sinnenschärfe": "!!TODO!!",
"skifahren": "!!TODO!!",
"stimmen_imitieren": "!!TODO!!",
"tanzen": "!!TODO!!",
"taschendiebstahl": "!!TODO!!",
"zechen": "!!TODO!!"
}
},
"gesellschaftliche": {
"label": "!!TODO!!",
"name": {
"betoeren": "!!TODO!!",
"etikette": "!!TODO!!",
"gassenwissen": "!!TODO!!",
"lehren": "!!TODO!!",
"menschenkenntnis": "!!TODO!!",
"schauspielerei": "!!TODO!!",
"schriftlicher_ausdruck": "!!TODO!!",
"sich_verkleiden": "!!TODO!!",
"ueberreden": "!!TODO!!",
"ueberzeugen": "!!TODO!!"
}
},
"natur": {
"label": "Nature Talents",
"name": {
"faehrtensuchen": "Track",
"fallenstellen": "Traps",
"fesseln": "Bind/Escape",
"fischen": "Fish",
"orientierung": "Orientation",
"wettervorhersage": "Weather Sense",
"wildnisleben": "Survival"
}
},
"wissens": {
"label": "!!TODO!!",
"name": {
"anatomie": "!!TODO!!",
"baukunst": "!!TODO!!"
}
},
"sprachen": {
"label": "!!TODO!!",
"name": {
"lesen_schreiben": "!!TODO!!",
"muttersprache": "!!TODO!!",
"fremdsprache": "!!TODO!!"
}
},
"handwerks": {
"label": "!!TODO!!",
"name": {
"abrichten": "!!TODO!!",
"ackerbau": "!!TODO!!"
}
}
},
"ruestungen": {
"kopf": "!!TODO!!",
"brust": "!!TODO!!",
"ruecken": "!!TODO!!",
"bauch": "!!TODO!!",
"linker_arm": "!!TODO!!",
"rechter_arm": "!!TODO!!",
"linkes_bein": "!!TODO!!",
"rechtes_bein": "!!TODO!!",
"gesamt_ruestungsschutz": "!!TODO!!",
"gesamt_behinderung": "!!TODO!!"
},
"bewaffnung": {
"bruchfaktor": "!!TODO!!",
"nahkampfwaffe": {
"label": "!!TODO!!",
"laenge": "!!TODO!!",
"trefferpunkte": "!!TODO!!",
"basis": "!!TODO!!",
"schwellenwert": "!!TODO!!",
"schadensschritte": "!!TODO!!",
"initiative": "!!TODO!!",
"modifikator": "!!TODO!!",
"attacke": "!!TODO!!",
"parade": "!!TODO!!",
"distanzklasse": "!!TODO!!",
"zweihaendig": "!!TODO!!",
"werfbar": "!!TODO!!",
"improvisiert": "!!TODO!!",
"priviligiert": "!!TODO!!",
"kampftalente": "!!TODO!!"
},
"parierwaffe": {
"label": "!!TODO!!",
"modifikator": "!!TODO!!",
"attacke": "!!TODO!!",
"parade": "!!TODO!!",
"initiative": "!!TODO!!"
},
"schild": {
"label": "!!TODO!!",
"groesse": {
"label": "!!TODO!!",
"klein": "!!TODO!!",
"gross": "!!TODO!!",
"sehr_gross": "!!TODO!!"
},
"modifikator": "!!TODO!!",
"attacke": "!!TODO!!",
"parade": "!!TODO!!",
"initiative": "!!TODO!!"
},
"fernkampfwaffe": {
"label": "!!TODO!!",
"trefferpunkte": "!!TODO!!",
"basis": "!!TODO!!",
"reichweiten": "!!TODO!!",
"modifikator": "!!TODO!!",
"laden": "!!TODO!!",
"munitionskosten": "!!TODO!!",
"munitionsgewicht": "!!TODO!!"
}
}
}
}
+654 -79
View File
@@ -1,98 +1,673 @@
.row {
display: flex;
flex-direction: row;
html {
font-size: 16px !important;
}
.col {
display: flex;
flex-direction: column;
}
.DSA41 {
/* Change from FoundryVTT's default of 'none' to 'auto' to allow checkboxes in the nav bar */
& .tabs > [data-tab] > * {
pointer-events: auto;
}
.wrap {
flex-wrap: wrap;
}
/* allow tabs to be visible on the right side of the application */
&.application.ActorSheet {
overflow: visible;
}
.center {
text-align: center;
}
& .window-content {
padding: 0;
overflow: visible;
height: calc(100% - var(--header-height));
}
& [data-action] {
cursor: pointer;
&:not(button):hover {
transform: scale(1.05);
}
}
.editable-input {
flex: 1;
padding: 0px 3px;
}
& .small {
font-size: 0.75em;
}
.editable-input input {
border: none;
}
.editable-number {
text-align: center;
}
.placeholder {
font-size: 0.8em;
border-top: 1px solid;
}
.character-image {
width: 115px;
height: 115px;
}
.die {
width: 48px;
height: 48px;
line-height: 48px;
background-color: #000;
mask-image: url("../src/Assets/d20.svg");
mask-size: contain;
& .left {
text-align: left;
justify-content: left;
justify-self: left;
}
text-align: center;
color: #fff;
& .right {
text-align: right;
justify-content: right;
justify-self: right;
}
& .center {
text-align: center;
justify-content: center;
align-content: center;
align-items: center;
}
& .align-center {
align-items: center;
}
& .fit-content {
width: fit-content;
height: fit-content;
}
& .row, &.row {
display: flex;
flex-direction: row;
flex: 1;
}
& .col {
display: flex;
flex-direction: column;
flex: 1;
}
& .noflex {
flex: 0;
}
& .gap {
gap: 0.5rem;
}
& .subgrid {
display: grid;
grid-template-rows: subgrid;
grid-template-columns: subgrid;
}
& .subgrid-columns {
display: grid;
grid-template-columns: subgrid;
}
& .subgrid-rows {
display: grid;
grid-template-rows: subgrid;
}
& .grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
& .grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
& .grid4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
& .grid5 { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); }
& .grid6 { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); }
& .grid7 { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
& .grid8 { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); }
& .grid9 { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); }
& .colspan2 { grid-column: span 2; }
& .colspan3 { grid-column: span 3; }
& .colspan4 { grid-column: span 4; }
& .colspan5 { grid-column: span 5; }
& .colspan6 { grid-column: span 6; }
& .colspan7 { grid-column: span 7; }
& .colspan8 { grid-column: span 8; }
& .colspan9 { grid-column: span 9; }
& .colspan-all { grid-column: 1 / -1; }
& .rowspan2 { grid-row: span 2; }
& .rowspan3 { grid-row: span 3; }
& .rowspan4 { grid-row: span 4; }
& .rowspan5 { grid-row: span 5; }
& .rowspan6 { grid-row: span 6; }
& .rowspan7 { grid-row: span 7; }
& .rowspan8 { grid-row: span 8; }
& .rowspan9 { grid-row: span 9; }
& .rowspan-all { grid-row: 1 / -1; }
& input {
border: none;
text-align: inherit;
&[type="number"] {
text-align: center;
}
}
& .price-input {
display: grid;
grid-template-columns: minmax(min-content, 1fr) max-content;
& input {
padding: 0;
}
}
& .weight-input {
display: grid;
grid-template-columns: minmax(min-content, 1fr) max-content;
& input {
padding: 0;
}
}
& .length-input {
display: grid;
grid-template-columns: minmax(min-content, 1fr) max-content;
& input {
padding: 0;
}
}
& .placeholder {
font-size: 0.8em;
border-top: 1px solid;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
& .subtitle {
font-size: 0.8em;
border-top: 1px solid;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
& .character-image {
width: 115px;
height: 115px;
}
& .item-image {
width: 80px;
height: 80px;
margin-right: 0.5rem;
}
& img {
object-fit: contain;
}
&.chat-header {
& img {
margin-right: .75em;
width: 38px;
height: 38px;
}
& .subtitle {
color: #666;
font-size: .6875rem;
}
}
&.chat-targets {
margin-top: 0.5em;
& img {
width: 20px;
height: 20px;
}
& .target {
display: grid;
grid-template-columns: max-content minmax(0, max-content) auto max-content;
gap: 0.5em;
align-items: center;
& button {
grid-column: 4;
line-height: normal;
}
}
}
& .die {
display: inline-grid;
text-align: center;
align-items: center;
color: #fff;
width: 3.5em;
height: 3.5em;
text-shadow: 1px 1px 1px black;
& > * {
grid-row: 1;
grid-column: 1;
}
&.die-type {
width: 2em;
height: 2em;
}
&.die-courage { fill: #b22319; }
&.die-cleverness { fill: #8158a3; }
&.die-intuition { fill: #388834; }
&.die-charisma { fill: #d96600; }
&.die-dexterity { fill: #d4b366; }
&.die-agility { fill: #678ec3; }
&.die-constitution { fill: #a3a3a3; }
&.die-strength { fill: #d5a877; }
&.die-attacke { fill: #b22319; }
&.die-parade { fill: #388834; }
&.die-trefferpunkte { fill: #a2a0ee; }
&.die-fernkampf-attacke { fill: #388834; }
&.die-fernkampf-trefferpunkte { fill: #a2a0ee; }
}
& .bar {
--bar-percentage: 100%;
--bar-color-left: #951a84;
--bar-color-right: #cd22b6;
position: relative;
overflow: hidden;
margin: 1em;
border: 1px solid #9f9275;
border-radius: 5px;
text-align: center;
&::before {
position: absolute;
z-index: -1;
left: 0;
width: var(--bar-percentage);
height: 100%;
content: "";
background: linear-gradient(90deg, var(--bar-color-left) 0%, var(--bar-color-right) 100%);
}
& input, & span {
display: inline-block;
background: transparent;
padding: 0;
width: 4ch;
}
&.hp {
--bar-color-left: #401f25;
--bar-color-right: #861212;
}
&.ausdauer {
--bar-color-left: #114f0c;
--bar-color-right: #178010;
}
&.astralenergie {
--bar-color-left: #0e1155;
--bar-color-right: #141cb7;
}
}
& .currency {
width: min-content;
margin-left: auto;
align-items: center;
display: grid;
grid-template-columns: repeat(8, 1fr);
& input {
display: inline-block;
background: transparent;
padding: 0;
width: 7ch;
}
& svg {
width: 25px;
height: 25px;
}
}
& .Abenteuerpunkte {
width: min-content;
margin-left: auto;
align-items: center;
display: flex;
gap: 1em;
& input {
display: inline-block;
background: transparent;
padding: 0;
width: 5ch;
}
}
& .tabs {
padding: .5rem;
margin-top: .5rem;
margin-bottom: .5rem;
border-top: 1px solid;
border-bottom: 1px solid;
line-height: normal;
& > * {
align-items: center;
}
& .active {
text-decoration: underline;
}
}
& .tab.active {
display: grid;
gap: 0.5rem;
}
& .list {
display: grid;
grid-template-rows: max-content;
grid-auto-rows: max-content;
border-radius: 5px 5px 5px 5px;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.45);
& .item-image {
width: 40px;
height: 40px;
}
}
& .list-header {
display: grid;
grid-column: 1 / -1;
grid-template-rows: subgrid;
grid-template-columns: subgrid;
align-items: center;
padding: 0.5rem;
border-radius: 5px 5px 0px 0px;
background: linear-gradient(90deg, rgb(25, 92, 30) 0%, rgb(0, 79, 7) 40%, rgb(0, 51, 5) 100%);
}
& .list-item {
display: grid;
grid-column: 1 / -1;
grid-template-rows: subgrid;
grid-template-columns: subgrid;
align-items: center;
background: #252830;
padding: 0.25rem;
border-bottom: 1px dotted;
&:last-child {
border: none;
border-radius: 0px 0px 5px 5px;
}
}
&.Dialog > .window-content {
gap: 1rem;
& > :first-child {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
align-items: center;
}
}
& [data-application-part]:not([data-application-part="ActorSheet"]) {
padding: 1rem;
}
& [data-application-part="Bewaffnung"] {
& .tab {
grid-template-columns: minmax(0, max-content) minmax(0, 1fr) minmax(0, max-content) minmax(0, 1fr);
align-items: center;
}
}
& [data-application-part="Ruestung"] {
& .tab {
grid-template-columns: minmax(0, max-content) minmax(0, 1fr) minmax(0, max-content) minmax(0, 1fr);
align-items: center;
}
}
& [data-application-part="Zauber"] {
& .tab {
grid-template-columns: minmax(0, max-content) minmax(0, 1fr) minmax(0, max-content) minmax(0, 1fr);
align-items: center;
border-top: 1px solid;
margin-top: 0.5em;
padding-top: 0.5em;
& multi-select {
display: grid;
grid-template-columns: 1fr min-content;
align-items: right;
& select {
grid-column: 2;
appearance: none;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
background-image: url("/systems/dsa-4th-edition/src/Assets/plus.svg");
}
}
}
}
& [data-application-part="ActorSheet"] {
height: 100%;
& .scroll-container {
height: 100%;
overflow-y: scroll;
}
& .ActorSheet {
padding: 1rem;
}
& .tabs {
display: flex;
flex-direction: column;
align-items: start;
position: absolute;
z-index: -1;
left: 100%;
border: none;
padding: 0;
gap: 2em;
& > * {
transform: none !important;
&::before {
background: var(--background);
padding: .5em .5em .5em .75em;
border-width: 1px 1px 1px 0px;
border-radius: 0 5px 5px 0;
border-style: solid;
transition: all 250ms ease;
}
&.active, &:hover {
text-decoration: none;
&::before {
padding: .5em .5em .5em 1.25em;
}
}
}
}
& .tab {
&[data-tab="eigenschaften"] {
& .Eigenschaften {
display: grid;
grid-template-columns: minmax(0, max-content) repeat(3, minmax(0, 1fr));
align-items: center;
column-gap: 0.5rem;
& .list-header :not(:nth-child(1)), .list-item :not(:nth-child(1)) {
text-align: center;
}
}
& .Basiswerte {
display: grid;
grid-template-columns: minmax(0, max-content) repeat(5, minmax(0, 1fr));
align-items: center;
column-gap: 0.5rem;
& .list-header :not(:nth-child(1)), .list-item :not(:nth-child(1)) {
text-align: center;
}
}
& .Sonderfertigkeiten {
grid-template-columns: minmax(min-content, max-content) auto min-content;
}
& .Vorteile {
grid-template-columns: minmax(min-content, max-content) auto min-content;
}
& .Nachteile {
grid-template-columns: minmax(min-content, max-content) auto min-content;
}
}
&[data-tab="talente"] {
grid-template-columns: minmax(0, max-content) repeat(2, minmax(0, 1fr)) min-content;
& > * {
grid-column: 1 / -1;
}
& .Kampftalente {
grid-template-columns: minmax(0, max-content) repeat(5, minmax(0, 1fr));
}
}
&[data-tab="inventar"] {
grid-template-columns: minmax(min-content, max-content) auto min-content minmax(min-content, max-content) min-content;
& > * {
grid-column: 1 / -1;
}
& [data-equipped="false"] {
color: #464c5f;
}
}
&[data-tab="kampf"] {
& .Bewaffnung {
grid-template-columns: minmax(0, max-content) repeat(3, minmax(0, auto));
& .die {
font-size: 12px;
}
}
& .Ruestung {
grid-template-columns: 2fr repeat(8, 1fr) repeat(2, 1.5fr);
text-align: center;
}
& .Sonderfertigkeiten {
grid-template-columns: minmax(min-content, max-content) auto min-content;
}
}
&[data-tab="zauber"] {
grid-template-columns: minmax(0, max-content) auto minmax(0, max-content) auto minmax(0, max-content) min-content;
& > * {
grid-column: 1 / -1;
}
}
}
}
& .dsa41-calculation {
font-size: 18px;
padding-top: 0.5rem;
}
}
.die-courage { background-color: #b22319; }
.die-cleverness { background-color: #8158a3; }
.die-intuition { background-color: #388834; }
.die-charisma { background-color: #0c0c0c; }
.die-dexterity { background-color: #d4b366; }
.die-agility { background-color: #678ec3; }
.die-constitution { background-color: #a3a3a3; }
.die-strength { background-color: #d5a877; }
.talent_chat_message {
& .info {
display: grid;
grid-template-columns: repeat(4, minmax(min-content, 1fr));
text-wrap: nowrap;
.actor-sheet fieldset {
width: 100%;
& > * {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
& > *:not(:first-child) {
text-align: center;
}
}
}
}
.actor-sheet table {
border: none;
background: none;
table-layout: fixed;
.zauber_chat_message {
& .info {
display: grid;
grid-template-columns: repeat(4, minmax(min-content, 1fr));
text-wrap: nowrap;
& > * {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
& > *:not(:first-child) {
text-align: center;
}
}
}
}
.item-sheet header {
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
align-items: center;
}
.item-sheet header img {
flex: 0 0 64px;
height: 64px;
margin-right: 6px;
}
#tooltip.DSA41 {
max-width: 650px;
text-align: left;
.item-sheet header input,
.item-sheet header div {
flex: 1;
height: 48px;
line-height: 48px;
margin: 8px;
font-size: 2em;
& h4 {
font-size: large;
}
}
+1527 -28
View File
File diff suppressed because it is too large Load Diff
+57 -5
View File
@@ -5,19 +5,71 @@
"esmodules": ["src/main.mjs"],
"styles": ["src/main.css"],
"documentTypes": {
"Actor": {
"Player": {}
},
"Item": {
"Gegenstand": {},
"Ruestung": {},
"Bewaffnung": {},
"Zauber": {},
"Talent": {},
"Kampftalent": {},
"Sonderfertigkeit": {},
"VorNachteil": {}
}
},
"packs": [
{
"system": "dsa-4th-edition",
"path": "packs/talente",
"type": "Item",
"name": "talente",
"label": "Talente"
},
{
"system": "dsa-4th-edition",
"path": "packs/ruestungen",
"type": "Item",
"name": "ruestungen",
"label": "Rüstungen"
},
{
"system": "dsa-4th-edition",
"path": "packs/bewaffnungen",
"type": "Item",
"name": "bewaffnungen",
"label": "Bewaffnungen"
}
],
"packFolders": [
{
"name": "DSA 4.1",
"packs": [
"talente",
"ruestungen",
"bewaffnungen"
]
}
],
"languages": [
{
"lang": "de",
"name": "German (Deutsch)",
"path": "src/lang/de.json"
},
{
"lang": "en",
"name": "English",
"path": "src/lang/en.json"
}
],
"compatibility": {
"minimum": "13",
"verified": "13"
},
"version": "0.1.5",
"manifest": "https://gitea.ammerhai.com/foundry/dsa-4th-edition/releases/download/latest/system.json",
"download": "https://gitea.ammerhai.com/foundry/dsa-4th-edition/releases/download/test2/dsa-4th-edition.zip"
-16
View File
@@ -1,16 +0,0 @@
{
"Actor": {
"types": [
"Player"
]
},
"Item": {
"types": [
"Generic Item",
"Melee Weapon",
"Ranged Weapon",
"Armor",
"Shield"
]
}
}
+496
View File
@@ -0,0 +1,496 @@
const std = @import("std");
const leveldb = @import("leveldb");
const foundry = @import("foundry.zig");
const system = @import("system.zig");
const talente: system.ItemCompendium = .{ .entries = &.{
// Körperliche Talente
.{ .Folder = .{ .name = "Körperliche Talente", .entries = &.{
.{ .Talent = .{ .name = "Akrobatik", .system = .{ .kategorie = .koerperliche, .attribute1 = .MU, .attribute2 = .GE, .attribute3 = .KK, .behinderung = "@BE * 2" } } },
.{ .Talent = .{ .name = "Athletik", .system = .{ .kategorie = .koerperliche, .attribute1 = .GE, .attribute2 = .KO, .attribute3 = .KK, .behinderung = "@BE * 2" } } },
.{ .Talent = .{ .name = "Fliegen", .system = .{ .kategorie = .koerperliche, .attribute1 = .MU, .attribute2 = .IN, .attribute3 = .GE, .behinderung = "@BE" } } },
.{ .Talent = .{ .name = "Gaukeleien", .system = .{ .kategorie = .koerperliche, .attribute1 = .MU, .attribute2 = .CH, .attribute3 = .FF, .behinderung = "@BE * 2" } } },
.{ .Talent = .{ .name = "Klettern", .system = .{ .kategorie = .koerperliche, .attribute1 = .MU, .attribute2 = .GE, .attribute3 = .KK, .behinderung = "@BE * 2" } } },
.{ .Talent = .{ .name = "Körperbeherrschung", .system = .{ .kategorie = .koerperliche, .attribute1 = .MU, .attribute2 = .IN, .attribute3 = .GE, .behinderung = "@BE * 2" } } },
.{ .Talent = .{ .name = "Reiten", .system = .{ .kategorie = .koerperliche, .attribute1 = .CH, .attribute2 = .GE, .attribute3 = .KK, .behinderung = "@BE - 2" } } },
.{ .Talent = .{ .name = "Schleichen", .system = .{ .kategorie = .koerperliche, .attribute1 = .MU, .attribute2 = .IN, .attribute3 = .GE, .behinderung = "@BE" } } },
.{ .Talent = .{ .name = "Schwimmen", .system = .{ .kategorie = .koerperliche, .attribute1 = .GE, .attribute2 = .KO, .attribute3 = .KK, .behinderung = "@BE * 2" } } },
.{ .Talent = .{ .name = "Selbstbeherrschung", .system = .{ .kategorie = .koerperliche, .attribute1 = .MU, .attribute2 = .KO, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Sich Verstecken", .system = .{ .kategorie = .koerperliche, .attribute1 = .MU, .attribute2 = .IN, .attribute3 = .GE, .behinderung = "@BE - 2" } } },
.{ .Talent = .{ .name = "Singen", .system = .{ .kategorie = .koerperliche, .attribute1 = .IN, .attribute2 = .CH, .attribute3 = .CH, .behinderung = "@BE - 3" } } },
.{ .Talent = .{ .name = "Singen", .system = .{ .kategorie = .koerperliche, .attribute1 = .IN, .attribute2 = .CH, .attribute3 = .KO, .behinderung = "@BE - 3" } } },
.{ .Talent = .{ .name = "Sinnenschärfe", .system = .{ .kategorie = .koerperliche, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Sinnenschärfe", .system = .{ .kategorie = .koerperliche, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Skifahren", .system = .{ .kategorie = .koerperliche, .attribute1 = .GE, .attribute2 = .GE, .attribute3 = .KO, .behinderung = "@BE - 2" } } },
.{ .Talent = .{ .name = "Stimmen Imitieren", .system = .{ .kategorie = .koerperliche, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "@BE - 4" } } },
.{ .Talent = .{ .name = "Tanzen", .system = .{ .kategorie = .koerperliche, .attribute1 = .CH, .attribute2 = .GE, .attribute3 = .GE, .behinderung = "@BE * 2" } } },
.{ .Talent = .{ .name = "Taschendiebstahl", .system = .{ .kategorie = .koerperliche, .attribute1 = .MU, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "@BE * 2" } } },
.{ .Talent = .{ .name = "Zechen", .system = .{ .kategorie = .koerperliche, .attribute1 = .IN, .attribute2 = .KO, .attribute3 = .KK, .behinderung = "" } } },
}}},
// Gesellschaftliche Talente
.{ .Folder = .{ .name = "Gesellschaftliche Talente", .entries = &.{
.{ .Talent = .{ .name = "Betören", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .IN, .attribute2 = .CH, .attribute3 = .CH, .behinderung = "BE - 2" } } },
.{ .Talent = .{ .name = "Etikette", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "BE - 2" } } },
.{ .Talent = .{ .name = "Gassenwissen", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "BE - 4" } } },
.{ .Talent = .{ .name = "Lehren", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Menschenkenntnis", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Schauspielerei", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .MU, .attribute2 = .KL, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Schriftlicher Ausdruck", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Sich Verkleiden", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .MU, .attribute2 = .CH, .attribute3 = .GE, .behinderung = "" } } },
.{ .Talent = .{ .name = "Überreden", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .MU, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Überzeugen", .system = .{ .kategorie = .gesellschaftliche, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "" } } },
}}},
// Natur Talente
.{ .Folder = .{ .name = "Natur-Talente", .entries = &.{
.{ .Talent = .{ .name = "Fährtensuchen", .system = .{ .kategorie = .natur, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Fährtensuchen", .system = .{ .kategorie = .natur, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .KO, .behinderung = "" } } },
.{ .Talent = .{ .name = "Fallenstellen", .system = .{ .kategorie = .natur, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Fesseln/Entfesseln", .system = .{ .kategorie = .natur, .attribute1 = .FF, .attribute2 = .GE, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Fischen/Angeln", .system = .{ .kategorie = .natur, .attribute1 = .IN, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Orientierung", .system = .{ .kategorie = .natur, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Wettervorhersage", .system = .{ .kategorie = .natur, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Wildnisleben", .system = .{ .kategorie = .natur, .attribute1 = .IN, .attribute2 = .GE, .attribute3 = .KO, .behinderung = "" } } },
}}},
// Wissens Talente
.{ .Folder = .{ .name = "Wissens Talente", .entries = &.{
.{ .Talent = .{ .name = "Anatomie", .system = .{ .kategorie = .wissens, .attribute1 = .MU, .attribute2 = .KL, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Baukunst", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Brett-/Kartenspiel", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Geographie", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Geschichtswissen", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Gesteinskunde", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Götter/Kulte", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Heraldik", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Hüttenkunde", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .KO, .behinderung = "" } } },
.{ .Talent = .{ .name = "Kriegskunst", .system = .{ .kategorie = .wissens, .attribute1 = .MU, .attribute2 = .KL, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Kryptographie", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Magiekunde", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Mechanik", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Pflanzenkunde", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Philosophie", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Rechnen", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Rechtskunde", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Sagen/Legenden", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Schätzen", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Sprachenkunde", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Staatskunst", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Sternkunde", .system = .{ .kategorie = .wissens, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Tierkunde", .system = .{ .kategorie = .wissens, .attribute1 = .MU, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
}}},
// Handwerks Talente
.{ .Folder = .{ .name = "Handwerks Talente", .entries = &.{
.{ .Talent = .{ .name = "Abrichten", .system = .{ .kategorie = .handwerks, .attribute1 = .MU, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Ackerbau", .system = .{ .kategorie = .handwerks, .attribute1 = .IN, .attribute2 = .FF, .attribute3 = .KO, .behinderung = "" } } },
.{ .Talent = .{ .name = "Alchimie", .system = .{ .kategorie = .handwerks, .attribute1 = .MU, .attribute2 = .KL, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Bergbau", .system = .{ .kategorie = .handwerks, .attribute1 = .IN, .attribute2 = .KO, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Bogenbau", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Boote Fahren", .system = .{ .kategorie = .handwerks, .attribute1 = .GE, .attribute2 = .KO, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Brauer", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Drucker", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Fahrzeug Lenken", .system = .{ .kategorie = .handwerks, .attribute1 = .IN, .attribute2 = .CH, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Falschspiel", .system = .{ .kategorie = .handwerks, .attribute1 = .MU, .attribute2 = .CH, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Feinmechanik", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Feuersteinbearbeitung", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Fleischer", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Gerber/Kürschner", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KO, .behinderung = "" } } },
.{ .Talent = .{ .name = "Glaskunst", .system = .{ .kategorie = .handwerks, .attribute1 = .FF, .attribute2 = .FF, .attribute3 = .KO, .behinderung = "" } } },
.{ .Talent = .{ .name = "Grobschmied", .system = .{ .kategorie = .handwerks, .attribute1 = .FF, .attribute2 = .KO, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Handel", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Hauswirtschaft", .system = .{ .kategorie = .handwerks, .attribute1 = .IN, .attribute2 = .CH, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Heilkunde Gift", .system = .{ .kategorie = .handwerks, .attribute1 = .MU, .attribute2 = .KL, .attribute3 = .IN, .behinderung = "" } } },
.{ .Talent = .{ .name = "Heilkunde Krankheiten", .system = .{ .kategorie = .handwerks, .attribute1 = .MU, .attribute2 = .KL, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Heilkunde Seele", .system = .{ .kategorie = .handwerks, .attribute1 = .IN, .attribute2 = .CH, .attribute3 = .CH, .behinderung = "" } } },
.{ .Talent = .{ .name = "Heilkunde Wunden", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .CH, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Holzbearbeitung", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Instrumentenbauer", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Kartographie", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .KL, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Kochen", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Kristallzucht", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Lederarbeiten", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Malen/Zeichnen", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Maurer", .system = .{ .kategorie = .handwerks, .attribute1 = .FF, .attribute2 = .GE, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Metallguss", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Musizieren", .system = .{ .kategorie = .handwerks, .attribute1 = .IN, .attribute2 = .CH, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Schlösser Knacken", .system = .{ .kategorie = .handwerks, .attribute1 = .IN, .attribute2 = .FF, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Schnaps Brennen", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Schneidern", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Seefahrt", .system = .{ .kategorie = .handwerks, .attribute1 = .FF, .attribute2 = .GE, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Seiler", .system = .{ .kategorie = .handwerks, .attribute1 = .FF, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Steinmetz", .system = .{ .kategorie = .handwerks, .attribute1 = .FF, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Steinschneider/Juwelier", .system = .{ .kategorie = .handwerks, .attribute1 = .IN, .attribute2 = .FF, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Stellmacher", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Stoffe Färben", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Tätowieren", .system = .{ .kategorie = .handwerks, .attribute1 = .IN, .attribute2 = .FF, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Töpfern", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .FF, .behinderung = "" } } },
.{ .Talent = .{ .name = "Viehzucht", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .IN, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Webkunst", .system = .{ .kategorie = .handwerks, .attribute1 = .FF, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Winzer", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
.{ .Talent = .{ .name = "Zimmermann", .system = .{ .kategorie = .handwerks, .attribute1 = .KL, .attribute2 = .FF, .attribute3 = .KK, .behinderung = "" } } },
}}},
// Kampftalente
.{ .Folder = .{ .name = "Kampftalente", .entries = &.{
.{ .Kampftalent = .{ .name = "Anderthalbhänder", .system = .{ .kategorie = .nahkampf, .steigern = .E, .behinderung = "@BE - 2" } } },
.{ .Kampftalent = .{ .name = "Armbrust", .system = .{ .kategorie = .fernkampf, .steigern = .C, .behinderung = "@BE - 5" } } },
.{ .Kampftalent = .{ .name = "Belagerungswaffen", .system = .{ .kategorie = .fernkampf, .steigern = .D, .behinderung = "" } } },
.{ .Kampftalent = .{ .name = "Blasrohr", .system = .{ .kategorie = .fernkampf, .steigern = .D, .behinderung = "@BE - 5" } } },
.{ .Kampftalent = .{ .name = "Bogen", .system = .{ .kategorie = .fernkampf, .steigern = .E, .behinderung = "@BE - 3" } } },
.{ .Kampftalent = .{ .name = "Diskus", .system = .{ .kategorie = .fernkampf, .steigern = .D, .behinderung = "@BE - 2" } } },
.{ .Kampftalent = .{ .name = "Dolche", .system = .{ .kategorie = .nahkampf, .steigern = .D, .behinderung = "@BE - 1" } } },
.{ .Kampftalent = .{ .name = "Fechtwaffen", .system = .{ .kategorie = .nahkampf, .steigern = .E, .behinderung = "@BE - 1" } } },
.{ .Kampftalent = .{ .name = "Hiebwaffen", .system = .{ .kategorie = .nahkampf, .steigern = .D, .behinderung = "@BE - 4" } } },
.{ .Kampftalent = .{ .name = "Infanteriewaffen", .system = .{ .kategorie = .nahkampf, .steigern = .D, .behinderung = "@BE - 3" } } },
.{ .Kampftalent = .{ .name = "Kettenstäbe", .system = .{ .kategorie = .nahkampf, .steigern = .E, .behinderung = "@BE - 1" } } },
.{ .Kampftalent = .{ .name = "Kettenwaffen", .system = .{ .kategorie = .nahkampf, .steigern = .D, .behinderung = "@BE - 3" } } },
// .{ .Kampftalent = .{ .name = "Lanzenreiten", .system = .{ .kategorie = .???, .steigern = .E, .behinderung = "" } } },
// .{ .Kampftalent = .{ .name = "Peitsche", .system = .{ .kategorie = .???, .steigern = .E, .behinderung = "@BE - 1" } } },
.{ .Kampftalent = .{ .name = "Raufen", .system = .{ .kategorie = .waffenlos, .steigern = .C, .behinderung = "@BE" } } },
.{ .Kampftalent = .{ .name = "Ringen", .system = .{ .kategorie = .waffenlos, .steigern = .D, .behinderung = "@BE" } } },
.{ .Kampftalent = .{ .name = "Säbel" , .system = .{ .kategorie = .nahkampf, .steigern = .D, .behinderung = "@BE - 2" } } },
.{ .Kampftalent = .{ .name = "Schleuder", .system = .{ .kategorie = .fernkampf, .steigern = .E, .behinderung = "@BE - 2" } } },
.{ .Kampftalent = .{ .name = "Schwerter", .system = .{ .kategorie = .nahkampf, .steigern = .E, .behinderung = "@BE - 2" } } },
.{ .Kampftalent = .{ .name = "Speere", .system = .{ .kategorie = .nahkampf, .steigern = .D, .behinderung = "@BE - 3" } } },
.{ .Kampftalent = .{ .name = "Stäbe", .system = .{ .kategorie = .nahkampf, .steigern = .D, .behinderung = "@BE - 2" } } },
.{ .Kampftalent = .{ .name = "Wurfbeile", .system = .{ .kategorie = .fernkampf, .steigern = .D, .behinderung = "@BE - 2" } } },
.{ .Kampftalent = .{ .name = "Wurfmesser", .system = .{ .kategorie = .fernkampf, .steigern = .C, .behinderung = "@BE - 3" } } },
.{ .Kampftalent = .{ .name = "Wurfspeere", .system = .{ .kategorie = .fernkampf, .steigern = .C, .behinderung = "@BE - 2" } } },
.{ .Kampftalent = .{ .name = "Zweihandflegel", .system = .{ .kategorie = .nahkampf, .steigern = .D, .behinderung = "@BE - 3" } } },
.{ .Kampftalent = .{ .name = "Zweihand-Hiebwaffen", .system = .{ .kategorie = .nahkampf, .steigern = .D, .behinderung = "@BE - 3" } } },
.{ .Kampftalent = .{ .name = "Zweihandschwerter/-säbel", .system = .{ .kategorie = .nahkampf, .steigern = .E, .behinderung = "@BE - 2" } } },
}}},
}};
const ruestungen: system.ItemCompendium = .{ .entries = &.{
.{ .Folder = .{ .name = "Kleidung", .entries = &.{
.{ .Ruestung = .{ .name = "Anaurak", .system = .{ .kopf = 1, .brust = 1, .ruecken = 1, .bauch = 1, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 1, .gesamt_behinderung = 4, .gewicht = .{ .value = 5, .unit = .stein }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Dicke Kleidung", .system = .{ .kopf = 0, .brust = 1, .ruecken = 1, .bauch = 1, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 0.9, .gesamt_behinderung = 0.9, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Fellumhang / Fuhrmannsmantel", .system = .{ .kopf = 0, .brust = 1, .ruecken = 2, .bauch = 0, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 0.9, .gesamt_behinderung = 0.9, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Lederhose", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 1, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 0.4, .gesamt_behinderung = 0.4, .gewicht = .{ .value = 2, .unit = .stein }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Lederweste / Pelzweste", .system = .{ .kopf = 0, .brust = 1, .ruecken = 1, .bauch = 1, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.6, .gesamt_behinderung = 0.6, .gewicht = .{ .value = 2, .unit = .stein }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Lederweste / Pelzweste (lang)", .system = .{ .kopf = 0, .brust = 1, .ruecken = 1, .bauch = 1, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 0.8, .gesamt_behinderung = 0.8, .gewicht = .{ .value = 2, .unit = .stein }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Hohe Stiefel", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 0.2, .gesamt_behinderung = 0.2, .gewicht = .{ .value = 2, .unit = .stein }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Tuchrüstungen", .entries = &.{
.{ .Ruestung = .{ .name = "Gambeson", .system = .{ .kopf = 0, .brust = 2, .ruecken = 2, .bauch = 2, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 1.5, .gesamt_behinderung = 1.5, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 40, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Mattenrücken", .system = .{ .kopf = 1, .brust = 1, .ruecken = 3, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.9, .gesamt_behinderung = 0.9, .gewicht = .{ .value = 3.5, .unit = .stein }, .preis = .{ .value = 65, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Tuchrüstung", .system = .{ .kopf = 0, .brust = 2, .ruecken = 2, .bauch = 2, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 1.2, .gesamt_behinderung = 1.2, .gewicht = .{ .value = 2.5, .unit = .stein }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Unterzeug mit Kettenteilen", .system = .{ .kopf = 0, .brust = 2, .ruecken = 2, .bauch = 1, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 1.4, .gesamt_behinderung = 1.4, .gewicht = .{ .value = 4, .unit = .stein }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Wattierte Kappe", .system = .{ .kopf = 1, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.1, .gesamt_behinderung = 0.1, .gewicht = .{ .value = 0.5, .unit = .stein }, .preis = .{ .value = 5, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Wattiertes Unterzeug", .system = .{ .kopf = 0, .brust = 1, .ruecken = 1, .bauch = 1, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 0.9, .gesamt_behinderung = 0.9, .gewicht = .{ .value = 2.5, .unit = .stein }, .preis = .{ .value = 25, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Lederrüstungen", .entries = &.{
.{ .Ruestung = .{ .name = "Armschienen, Leder", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.1, .gesamt_behinderung = 0.1, .gewicht = .{ .value = 1, .unit = .stein }, .preis = .{ .value = 15, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Beinschienen, Leder", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 0.2, .gesamt_behinderung = 0.2, .gewicht = .{ .value = 1, .unit = .stein }, .preis = .{ .value = 25, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Brustplatte", .system = .{ .kopf = 0, .brust = 2, .ruecken = 0, .bauch = 1, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.6, .gesamt_behinderung = 0.6, .gewicht = .{ .value = 2, .unit = .stein }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Iryanrüstung", .system = .{ .kopf = 0, .brust = 3, .ruecken = 2, .bauch = 2, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 1.6, .gesamt_behinderung = 0.6, .gewicht = .{ .value = 3.5, .unit = .stein }, .preis = .{ .value = 125, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Krötenhaut", .system = .{ .kopf = 0, .brust = 3, .ruecken = 2, .bauch = 2, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 1.5, .gesamt_behinderung = 0.5, .gewicht = .{ .value = 4, .unit = .stein }, .preis = .{ .value = 60, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Lederharnisch", .system = .{ .kopf = 0, .brust = 3, .ruecken = 3, .bauch = 3, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 1.8, .gesamt_behinderung = 1.8, .gewicht = .{ .value = 4.5, .unit = .stein }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Lederhelm", .system = .{ .kopf = 2, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.2, .gesamt_behinderung = 0.2, .gewicht = .{ .value = 1.5, .unit = .stein }, .preis = .{ .value = 20, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Lederhelm, verstärkt", .system = .{ .kopf = 3, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.3, .gesamt_behinderung = 0.3, .gewicht = .{ .value = 1.75, .unit = .stein }, .preis = .{ .value = 30, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Streifenschurz", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 2, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 2, .rechtes_bein = 2, .gesamt_ruestungsschutz = 0.8, .gesamt_behinderung = 0.4, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 40, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Exotische Materialien", .entries = &.{
.{ .Ruestung = .{ .name = "Mammutonpanzer", .system = .{ .kopf = 0, .brust = 4, .ruecken = 4, .bauch = 4, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 2, .rechtes_bein = 2, .gesamt_ruestungsschutz = 3, .gesamt_behinderung = 2, .gewicht = .{ .value = 6, .unit = .stein }, .preis = .{ .value = 1500, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Maraskanischer Hartholzharnisch", .system = .{ .kopf = 0, .brust = 4, .ruecken = 4, .bauch = 4, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 2.7, .gesamt_behinderung = 1.7, .gewicht = .{ .value = 7, .unit = .stein }, .preis = .{ .value = 1200, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Kette/Schuppe", .entries = &.{
.{ .Ruestung = .{ .name = "Brabaker Ringmantel", .system = .{ .kopf = 0, .brust = 3, .ruecken = 3, .bauch = 3, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 2, .rechtes_bein = 2, .gesamt_ruestungsschutz = 2.4, .gesamt_behinderung = 1.4, .gewicht = .{ .value = 9, .unit = .stein }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Brigantina", .system = .{ .kopf = 0, .brust = 5, .ruecken = 4, .bauch = 4, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 2.8, .gesamt_behinderung = 2.8, .gewicht = .{ .value = 6, .unit = .stein }, .preis = .{ .value = 350, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Eisenmantel", .system = .{ .kopf = 0, .brust = 5, .ruecken = 5, .bauch = 5, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 2, .rechtes_bein = 2, .gesamt_ruestungsschutz = 3.6, .gesamt_behinderung = 2.6, .gewicht = .{ .value = 6, .unit = .stein }, .preis = .{ .value = 500, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Fünflagenharnisch", .system = .{ .kopf = 0, .brust = 5, .ruecken = 5, .bauch = 4, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 3, .gesamt_behinderung = 3, .gewicht = .{ .value = 7, .unit = .stein }, .preis = .{ .value = 600, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kettenbeinlinge, Paar", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 4, .rechtes_bein = 4, .gesamt_ruestungsschutz = 0.8, .gesamt_behinderung = 0.8, .gewicht = .{ .value = 8, .unit = .stein }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kettenhandschuhe, Paar", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.1, .gesamt_behinderung = 0.1, .gewicht = .{ .value = 1.5, .unit = .stein }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kettenhaube", .system = .{ .kopf = 3, .brust = 1, .ruecken = 1, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.7, .gesamt_behinderung = 0.7, .gewicht = .{ .value = 3.5, .unit = .stein }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kettenhaube, mit Gesichtsschutz", .system = .{ .kopf = 4, .brust = 1, .ruecken = 1, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.8, .gesamt_behinderung = 0.8, .gewicht = .{ .value = 4, .unit = .stein }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kettenhemd, 1/2 Arm", .system = .{ .kopf = 0, .brust = 4, .ruecken = 4, .bauch = 4, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 2.8, .gesamt_behinderung = 1.8, .gewicht = .{ .value = 6.5, .unit = .stein }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kettenhemd, lang", .system = .{ .kopf = 0, .brust = 4, .ruecken = 4, .bauch = 4, .linker_arm = 3, .rechter_arm = 3, .linkes_bein = 2, .rechtes_bein = 2, .gesamt_ruestungsschutz = 3.1, .gesamt_behinderung = 2.1, .gewicht = .{ .value = 10, .unit = .stein }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kettenmantel", .system = .{ .kopf = 0, .brust = 4, .ruecken = 4, .bauch = 4, .linker_arm = 3, .rechter_arm = 3, .linkes_bein = 3, .rechtes_bein = 3, .gesamt_ruestungsschutz = 3.3, .gesamt_behinderung = 2.3, .gewicht = .{ .value = 12, .unit = .stein }, .preis = .{ .value = 500, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kettenkragen", .system = .{ .kopf = 2, .brust = 1, .ruecken = 1, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.6, .gesamt_behinderung = 0.3, .gewicht = .{ .value = 2.5, .unit = .stein }, .preis = .{ .value = 60, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kettenweste", .system = .{ .kopf = 0, .brust = 4, .ruecken = 4, .bauch = 4, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 2.4, .gesamt_behinderung = 1.4, .gewicht = .{ .value = 5, .unit = .stein }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Löwenmähne", .system = .{ .kopf = 2, .brust = 2, .ruecken = 2, .bauch = 0, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 1.1, .gesamt_behinderung = 0.55, .gewicht = .{ .value = 5, .unit = .stein }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Ringelpanzer", .system = .{ .kopf = 0, .brust = 4, .ruecken = 4, .bauch = 4, .linker_arm = 3, .rechter_arm = 3, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 2.9, .gesamt_behinderung = 1.9, .gewicht = .{ .value = 7, .unit = .stein }, .preis = .{ .value = 550, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Schuppenpanzer", .system = .{ .kopf = 0, .brust = 5, .ruecken = 5, .bauch = 5, .linker_arm = 3, .rechter_arm = 3, .linkes_bein = 3, .rechtes_bein = 3, .gesamt_ruestungsschutz = 3.9, .gesamt_behinderung = 3.9, .gewicht = .{ .value = 12, .unit = .stein }, .preis = .{ .value = 1000, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Schuppenpanzer, lang", .system = .{ .kopf = 0, .brust = 5, .ruecken = 5, .bauch = 5, .linker_arm = 3, .rechter_arm = 3, .linkes_bein = 4, .rechtes_bein = 4, .gesamt_ruestungsschutz = 4.1, .gesamt_behinderung = 3.1, .gewicht = .{ .value = 18, .unit = .stein }, .preis = .{ .value = 1200, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Spiegelpanzer", .system = .{ .kopf = 0, .brust = 5, .ruecken = 5, .bauch = 5, .linker_arm = 3, .rechter_arm = 3, .linkes_bein = 2, .rechtes_bein = 2, .gesamt_ruestungsschutz = 3.7, .gesamt_behinderung = 2.7, .gewicht = .{ .value = 10, .unit = .stein }, .preis = .{ .value = 1000, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Plattenrüstungen", .entries = &.{
.{ .Ruestung = .{ .name = "Amazonenrüstung", .system = .{ .kopf = 3, .brust = 5, .ruecken = 3, .bauch = 5, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 3, .rechtes_bein = 3, .gesamt_ruestungsschutz = 3.7, .gesamt_behinderung = 1.7, .gewicht = .{ .value = 8, .unit = .stein }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Armschienen, Bronze", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.2, .gesamt_behinderung = 0.2, .gewicht = .{ .value = 1.5, .unit = .stein }, .preis = .{ .value = 25, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Armschienen, Stahl", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 3, .rechter_arm = 3, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.3, .gesamt_behinderung = 0.3, .gewicht = .{ .value = 1.5, .unit = .stein }, .preis = .{ .value = 35, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Baburiner Hut", .system = .{ .kopf = 4, .brust = 0, .ruecken = 1, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.6, .gesamt_behinderung = 0.3, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 60, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Bart / Halsberge", .system = .{ .kopf = 2, .brust = 1, .ruecken = 1, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.6, .gesamt_behinderung = 0.6, .gewicht = .{ .value = 1, .unit = .stein }, .preis = .{ .value = 45, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Beinschienen, Bronze", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 2, .rechtes_bein = 2, .gesamt_ruestungsschutz = 0.4, .gesamt_behinderung = 0.4, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 35, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Beinschienen, Stahl", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 3, .rechtes_bein = 3, .gesamt_ruestungsschutz = 0.6, .gesamt_behinderung = 0.6, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Beintaschen / Schürze", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 2, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 2, .rechtes_bein = 2, .gesamt_ruestungsschutz = 0.8, .gesamt_behinderung = 0.8, .gewicht = .{ .value = 2, .unit = .stein }, .preis = .{ .value = 90, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Bronzeharnisch", .system = .{ .kopf = 0, .brust = 5, .ruecken = 4, .bauch = 4, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 2.6, .gesamt_behinderung = 2.6, .gewicht = .{ .value = 6, .unit = .stein }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Brustplatte", .system = .{ .kopf = 0, .brust = 2, .ruecken = 0, .bauch = 1, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.6, .gesamt_behinderung = 0.6, .gewicht = .{ .value = 2, .unit = .stein }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Brustschalen", .system = .{ .kopf = 0, .brust = 2, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.4, .gesamt_behinderung = 0.4, .gewicht = .{ .value = 0.5, .unit = .stein }, .preis = .{ .value = 25, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Drachenhelm", .system = .{ .kopf = 3, .brust = 0, .ruecken = 1, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.5, .gesamt_behinderung = 0.5, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Garether Platte", .system = .{ .kopf = 0, .brust = 6, .ruecken = 5, .bauch = 6, .linker_arm = 5, .rechter_arm = 5, .linkes_bein = 4, .rechtes_bein = 4, .gesamt_ruestungsschutz = 4.7, .gesamt_behinderung = 3.7, .gewicht = .{ .value = 14, .unit = .stein }, .preis = .{ .value = 750, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Gestechrüstung", .system = .{ .kopf = 8, .brust = 8, .ruecken = 7, .bauch = 8, .linker_arm = 7, .rechter_arm = 7, .linkes_bein = 7, .rechtes_bein = 7, .gesamt_ruestungsschutz = 7.5, .gesamt_behinderung = 7.5, .gewicht = .{ .value = 30, .unit = .stein }, .preis = .{ .value = 2500, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Gladiatorenschulter", .system = .{ .kopf = 0, .brust = 3, .ruecken = 2, .bauch = 0, .linker_arm = 3, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 1.15, .gesamt_behinderung = 0.15, .gewicht = .{ .value = 4, .unit = .stein }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Horasischer Reiterharnisch", .system = .{ .kopf = 3, .brust = 7, .ruecken = 5, .bauch = 7, .linker_arm = 5, .rechter_arm = 5, .linkes_bein = 5, .rechtes_bein = 5, .gesamt_ruestungsschutz = 5.6, .gesamt_behinderung = 3.6, .gewicht = .{ .value = 17, .unit = .stein }, .preis = .{ .value = 1000, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kürass", .system = .{ .kopf = 0, .brust = 5, .ruecken = 1, .bauch = 2, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 1.6, .gesamt_behinderung = 0.6, .gewicht = .{ .value = 4, .unit = .stein }, .preis = .{ .value = 110, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Kusliker Lamellar", .system = .{ .kopf = 0, .brust = 5, .ruecken = 4, .bauch = 4, .linker_arm = 1, .rechter_arm = 1, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 2.9, .gesamt_behinderung = 2.9, .gewicht = .{ .value = 7.5, .unit = .stein }, .preis = .{ .value = 500, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Leichte Platte", .system = .{ .kopf = 0, .brust = 5, .ruecken = 4, .bauch = 5, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 2, .rechtes_bein = 2, .gesamt_ruestungsschutz = 3.2, .gesamt_behinderung = 2.2, .gewicht = .{ .value = 7.5, .unit = .stein }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Morion", .system = .{ .kopf = 3, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.3, .gesamt_behinderung = 0.15, .gewicht = .{ .value = 4, .unit = .stein }, .preis = .{ .value = 75, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Panzerbein", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 4, .rechtes_bein = 4, .gesamt_ruestungsschutz = 0.8, .gesamt_behinderung = 0.8, .gewicht = .{ .value = 6, .unit = .stein }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Panzerhandschuhe, Paar", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.2, .gesamt_behinderung = 0.2, .gewicht = .{ .value = 1.5, .unit = .stein }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Panzerschuh", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 1, .rechtes_bein = 1, .gesamt_ruestungsschutz = 0.2, .gesamt_behinderung = 0.2, .gewicht = .{ .value = 1, .unit = .stein }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Plattenschultern", .system = .{ .kopf = 0, .brust = 1, .ruecken = 1, .bauch = 0, .linker_arm = 2, .rechter_arm = 2, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.6, .gesamt_behinderung = 0.6, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Plattenarme", .system = .{ .kopf = 0, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 5, .rechter_arm = 5, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.5, .gesamt_behinderung = 0.5, .gewicht = .{ .value = 3, .unit = .stein }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Schaller", .system = .{ .kopf = 4, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.4, .gesamt_behinderung = 0.2, .gewicht = .{ .value = 4, .unit = .stein }, .preis = .{ .value = 60, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Stechhelm / Visierhelm", .system = .{ .kopf = 5, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.5, .gesamt_behinderung = 0.5, .gewicht = .{ .value = 4, .unit = .stein }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Sturmhaube", .system = .{ .kopf = 3, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.3, .gesamt_behinderung = 0.15, .gewicht = .{ .value = 3.5, .unit = .stein }, .preis = .{ .value = 70, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Tellerhelm", .system = .{ .kopf = 2, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.2, .gesamt_behinderung = 0.2, .gewicht = .{ .value = 1.5, .unit = .stein }, .preis = .{ .value = 30, .unit = .silbertaler } } } },
.{ .Ruestung = .{ .name = "Topfhelm", .system = .{ .kopf = 5, .brust = 0, .ruecken = 0, .bauch = 0, .linker_arm = 0, .rechter_arm = 0, .linkes_bein = 0, .rechtes_bein = 0, .gesamt_ruestungsschutz = 0.5, .gesamt_behinderung = 0.5, .gewicht = .{ .value = 4.5, .unit = .stein }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
}}},
}};
const bewaffnungen: system.ItemCompendium = .{ .entries = &.{
.{ .Folder = .{ .name = "Anderthalbhänder", .entries = &.{
.{ .Bewaffnung = .{ .name = "Anderthalbhänder", .img = "icons/weapons/swords/greatsword-crossguard-engraved-green.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 1, .distanzklasse = "NS", .kampftalente = "Anderthalbhänder", .laenge = .{ .value = 115, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Bastardschwert", .img = "icons/weapons/swords/greatsword-crossguard-barbed.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 11, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Anderthalbhänder", .laenge = .{ .value = 110, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Nachtwind", .img = "icons/weapons/swords/sword-katana.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 11, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 2, .bruchfaktor = 0, .distanzklasse = "N", .kampftalente = "Anderthalbhänder", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 500, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Rondrakamm", .img = "icons/weapons/swords/greatsword-flamberge.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+2", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "NS", .kampftalente = "Anderthalbhänder", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = .{ .value = 130, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Tuzakmesser", .img = "icons/weapons/swords/sword-katana.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+6", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 1, .distanzklasse = "NS", .kampftalente = "Anderthalbhänder", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 400, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Dolche", .entries = &.{
.{ .Bewaffnung = .{ .name = "Basiliskenzunge", .img = "icons/weapons/daggers/dagger-crooked-grey.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 4, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 30, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 25, .unit = .unze }, .preis = .{ .value = 70, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Borndorn", .img = "icons/weapons/daggers/dagger-straight-cracked.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 40, .unit = .halbfinger }, .zweihaendig = false, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 40, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Dolch", .img = "icons/weapons/daggers/dagger-straight-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 30, .unit = .halbfinger }, .zweihaendig = false, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 20, .unit = .unze }, .preis = .{ .value = 20, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Drachenzahn", .img = "icons/weapons/daggers/dagger-jeweled-purple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 0, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 40, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 40, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Eberfänger", .img = "icons/weapons/daggers/dagger-double-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 40, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 40, .unit = .unze }, .preis = .{ .value = 60, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Hakendolch", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 1, .initiative = 0, .bruchfaktor = -2, .distanzklasse = "HN", .kampftalente = "Dolche", .laenge = .{ .value = 60, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 50, .unit = .unze }, .preis = .{ .value = 90, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Jagdmesser", .img = "icons/weapons/swords/sword-broad-worn.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 3, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 30, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 15, .unit = .unze }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kurzschwert", .img = "icons/weapons/swords/shortsword-green.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "HN", .kampftalente = "Dolche", .laenge = .{ .value = 50, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 40, .unit = .unze }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Langdolch", .img = "icons/weapons/swords/shortsword-guard-steel-worn.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 40, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 45, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Linkhand", .img = "icons/weapons/daggers/dagger-straight-thin-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = 1, .initiative = 0, .bruchfaktor = 0, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 30, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 90, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Mengbilar", .img = "icons/weapons/daggers/dagger-straight-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 7, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 25, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 20, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Messer", .img = "icons/weapons/daggers/knife-green.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6", .schwellenwert = 12, .schadensschritte = 6, .modifikator_attacke = -2, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 4, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 25, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 10, .unit = .unze }, .preis = .{ .value = 10, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Ogerfänger", .img = "icons/weapons/daggers/dagger-serrated-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = 0, .bruchfaktor = 4, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 35, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 35, .unit = .unze }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Scheibendolch", .img = "icons/weapons/daggers/dagger-simple-green.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -3, .initiative = 0, .bruchfaktor = 0, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 45, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 40, .unit = .unze }, .preis = .{ .value = 60, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Schwerer Dolch", .img = "icons/weapons/daggers/dagger-straight-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 35, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 40, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Vulkanglasdolch", .img = "icons/weapons/daggers/dagger-simple-stone-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6-1", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -2, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 6, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 30, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Waqqif", .img = "icons/weapons/daggers/dagger-curved-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 2, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 45, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 35, .unit = .unze }, .preis = .{ .value = 60, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Wurfdolch", .img = "icons/weapons/thrown/daggers-kunai-purple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 25, .unit = .halbfinger }, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 20, .unit = .unze }, .preis = .{ .value = 30, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Wurfmesser", .img = "icons/weapons/thrown/dagger-simple-wood.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6-1", .schwellenwert = 12, .schadensschritte = 6, .modifikator_attacke = -2, .modifikator_parade = -3, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "H", .kampftalente = "Dolche", .laenge = .{ .value = 20, .unit = .halbfinger }, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 10, .unit = .unze }, .preis = .{ .value = 15, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Fechtwaffen", .entries = &.{
.{ .Bewaffnung = .{ .name = "Degen", .img = "icons/weapons/swords/sword-guard-brass-worn.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 2, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Fechtwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 40, .unit = .unze }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Florett", .img = "icons/weapons/swords/sword-guard-brass-worn.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 13, .schadensschritte = 5, .modifikator_attacke = 1, .modifikator_parade = -1, .initiative = 3, .bruchfaktor = 4, .distanzklasse = "N", .kampftalente = "Fechtwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Langdolch", .img = "icons/weapons/swords/shortsword-guard-steel-worn.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "H", .kampftalente = "Fechtwaffen", .laenge = .{ .value = 40, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 45, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Magierdegen", .img = "icons/weapons/swords/sword-guard-brass-worn.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 13, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = 1, .bruchfaktor = 4, .distanzklasse = "N", .kampftalente = "Fechtwaffen", .laenge = .{ .value = 75, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Panzerstecher", .img = "icons/weapons/swords/sword-guard-brass-worn.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = -1, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 0, .distanzklasse = "N", .kampftalente = "Fechtwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Rapier", .img = "icons/weapons/swords/sword-guard-red-jewel.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Fechtwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 45, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Robbentöter", .img = "icons/weapons/swords/sword-jeweled-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Fechtwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Stockdegen", .img = "icons/weapons/swords/sword-guard-brass-worn.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = 0, .bruchfaktor = 4, .distanzklasse = "N", .kampftalente = "Fechtwaffen", .laenge = .{ .value = 80, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 35, .unit = .unze }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Wolfsmesser", .img = "icons/weapons/swords/sword-jeweled-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Fechtwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 50, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Hiebwaffen", .entries = &.{
.{ .Bewaffnung = .{ .name = "Baccanaq / Bakka", .img = "icons/commodities/claws/claw-bear-brown.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 5, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 80, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Beil", .img = "icons/tools/hand/hatchet-steel-grey.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = -1, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 5, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 50, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 20, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Brabakbengel", .img = "icons/weapons/maces/mace-round-spiked-grey.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Byakka", .img = "icons/weapons/axes/axe-double-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 14, .schadensschritte = 2, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 130, .unit = .unze }, .preis = .{ .value = 90, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Fackel", .img = "icons/sundries/lights/torch-brown-lit.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6", .schwellenwert = 11, .schadensschritte = 5, .modifikator_attacke = -2, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 8, .distanzklasse = "HN", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 50, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 0.5, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Fleischerbeil", .img = "icons/tools/cooking/knife-cleaver-steel-grey.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = -2, .modifikator_parade = -3, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "H", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 30, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 60, .unit = .unze }, .preis = .{ .value = 20, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Haumesser", .img = "icons/weapons/swords/sword-guard-gold-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 3, .distanzklasse = "HN", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 50, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 40, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Keule", .img = "icons/weapons/clubs/club-barbed.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 11, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 80, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 15, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Knochenkeule", .img = "icons/weapons/clubs/club-bone-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 11, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 110, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Knüppel", .img = "icons/weapons/clubs/club-simple-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = 0, .bruchfaktor = 6, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 80, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 60, .unit = .unze }, .preis = .{ .value = 1, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kriegsfächer", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = 1, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "H", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 40, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 50, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Lindwurmschläger", .img = "icons/weapons/axes/shortaxe-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 11, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 1, .distanzklasse = "HN", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 50, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 95, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Molokdeschnaja", .img = "icons/weapons/axes/axe-broad-grey.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 11, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 90, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Orknase", .img = "icons/weapons/polearms/halberd-engraved-steel.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 4, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 110, .unit = .unze }, .preis = .{ .value = 75, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Rabenschnabel", .img = "icons/weapons/hammers/hammer-war-rounding.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 10, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 110, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 130, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Schmiedehammer", .img = "icons/weapons/hammers/shorthammer-embossed-white.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 14, .schadensschritte = 2, .modifikator_attacke = -1, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Sichel", .img = "icons/weapons/sickles/sickle-curved.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -2, .modifikator_parade = -2, .initiative = -2, .bruchfaktor = 6, .distanzklasse = "H", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 50, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 25, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Skraja", .img = "icons/weapons/axes/axe-double-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 11, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 4, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 70, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Sonnenszepter", .img = "icons/magic/light/torch-fire-orange.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = -1, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 70, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Streitaxt", .img = "icons/weapons/axes/axe-battle-blackened.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 13, .schadensschritte = 2, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Streitkolben", .img = "icons/weapons/maces/mace-spiked-cube-wood.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 11, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 75, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Stuhlbein", .img = "icons/commodities/wood/lumber-plank-beige.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6", .schwellenwert = 11, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 8, .distanzklasse = "HN", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 40, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 40, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Wurfbeil", .img = "icons/weapons/axes/axe-broad-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 10, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "H", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 40, .unit = .halbfinger }, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 50, .unit = .unze }, .preis = .{ .value = 35, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Wurfkeule", .img = "icons/weapons/clubs/club-baton-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 3, .distanzklasse = "H", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 40, .unit = .halbfinger }, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 35, .unit = .unze }, .preis = .{ .value = 18, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Zwergenskraja", .img = "icons/weapons/axes/axe-double-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 11, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "HN", .kampftalente = "Hiebwaffen", .laenge = .{ .value = 60, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Infanteriewaffen", .entries = &.{
.{ .Bewaffnung = .{ .name = "Glefe", .img = "icons/weapons/polearms/glaive-simple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 200, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 45, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Hakenspieß", .img = "icons/weapons/polearms/spear-hooked-spike.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 13, .schadensschritte = 4, .modifikator_attacke = -1, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 250, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 70, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Hellebarde", .img = "icons/weapons/polearms/halberd-engraved-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 200, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 75, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Korspieß", .img = "icons/weapons/staves/staff-hooked-banded.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+2", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 180, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = .{ .value = 140, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Neethaner Langaxt", .img = "icons/weapons/polearms/glaive-hooked-steel.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+2", .schwellenwert = 13, .schadensschritte = 4, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 180, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 160, .unit = .unze }, .preis = .{ .value = 160, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Pailos", .img = "icons/weapons/polearms/halberd-crescent-wood.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+4", .schwellenwert = 14, .schadensschritte = 2, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 3, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 175, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 180, .unit = .unze }, .preis = .{ .value = 300, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Partisane", .img = "icons/weapons/polearms/pike-flared-brown.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = 0, .bruchfaktor = 4, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 200, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Schnitter", .img = "icons/weapons/polearms/glaive-simple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 14, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 4, .distanzklasse = "NS", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Sense", .img = "icons/weapons/sickles/scythe-wrapped-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 13, .schadensschritte = 4, .modifikator_attacke = -2, .modifikator_parade = -4, .initiative = -2, .bruchfaktor = 7, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 160, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 30, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Sturmsense", .img = "icons/weapons/polearms/glaive-simple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = -1, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 180, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 40, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Warunker Hammer", .img = "icons/weapons/hammers/hammer-war-rounding.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+6", .schwellenwert = 14, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "NS", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 150, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Wurmspieß", .img = "icons/weapons/polearms/pike-flared-brown.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 13, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "S", .kampftalente = "Infanteriewaffen", .laenge = .{ .value = 180, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Kettenstäbe", .entries = &.{
.{ .Bewaffnung = .{ .name = "Dreigliederstab", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 13, .schadensschritte = 4, .modifikator_attacke = 1, .modifikator_parade = 1, .initiative = 2, .bruchfaktor = 3, .distanzklasse = "HN", .kampftalente = "Kettenstäbe", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kettenstab", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 13, .schadensschritte = 4, .modifikator_attacke = 1, .modifikator_parade = 0, .initiative = 2, .bruchfaktor = 2, .distanzklasse = "HN", .kampftalente = "Kettenstäbe", .laenge = .{ .value = 120, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Kettenwaffen", .entries = &.{
.{ .Bewaffnung = .{ .name = "Geißel", .img = "icons/weapons/misc/whip-red-yellow.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6-1", .schwellenwert = 14, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = -4, .initiative = -1, .bruchfaktor = 5, .distanzklasse = "N", .kampftalente = "Kettenwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 30, .unit = .unze }, .preis = .{ .value = 15, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kettenkugel", .img = "icons/sundries/survival/cuffs-shackles-ball.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "3d6", .schwellenwert = 16, .schadensschritte = 2, .modifikator_attacke = -2, .modifikator_parade = -4, .initiative = -3, .bruchfaktor = 2, .distanzklasse = "S", .kampftalente = "Kettenwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 250, .unit = .unze }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Morgenstern", .img = "icons/weapons/maces/flail-spiked-grey.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 14, .schadensschritte = 2, .modifikator_attacke = -1, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Kettenwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 140, .unit = .unze }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Neunschwänzige", .img = "icons/weapons/misc/whip-red-yellow.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 14, .schadensschritte = 4, .modifikator_attacke = -1, .modifikator_parade = -4, .initiative = -1, .bruchfaktor = 5, .distanzklasse = "N", .kampftalente = "Kettenwaffen", .laenge = .{ .value = 120, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 60, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Ochsenherde", .img = "icons/weapons/maces/flail-triple-grey.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "3d6+3", .schwellenwert = 17, .schadensschritte = 1, .modifikator_attacke = -2, .modifikator_parade = -4, .initiative = -3, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Kettenwaffen", .laenge = .{ .value = 110, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 300, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Ogerschelle", .img = "icons/weapons/maces/flail-triple-grey.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+2", .schwellenwert = 15, .schadensschritte = 1, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Kettenwaffen", .laenge = .{ .value = 120, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 240, .unit = .unze }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Peitsche", .entries = &.{
.{ .Bewaffnung = .{ .name = "Peitsche", .img = "icons/weapons/misc/whip-red-yellow.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6", .schwellenwert = 14, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 4, .distanzklasse = "S", .kampftalente = "Peitsche", .laenge = .{ .value = 250, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 60, .unit = .unze }, .preis = .{ .value = 25, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Säbel", .entries = &.{
.{ .Bewaffnung = .{ .name = "Amazonensäbel", .img = "icons/weapons/swords/scimitar-worn-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Säbel", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 75, .unit = .unze }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Arbach", .img = "icons/weapons/swords/scimitar-guard.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Säbel", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Entermesser", .img = "icons/weapons/swords/scimitar-guard-gold.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Säbel", .laenge = .{ .value = 75, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Haumesser", .img = "icons/weapons/swords/sword-guard-gold-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 3, .distanzklasse = "HN", .kampftalente = "Säbel", .laenge = .{ .value = 50, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 40, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Khunchomer", .img = "icons/weapons/swords/scimitar-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Säbel", .laenge = .{ .value = 80, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 130, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kurzschwert", .img = "icons/weapons/swords/shortsword-green.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "HN", .kampftalente = "Säbel", .laenge = .{ .value = 50, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 40, .unit = .unze }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kusliker Säbel", .img = "icons/weapons/swords/sword-guard-red-jewel.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Säbel", .laenge = .{ .value = 80, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 160, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Robbentöter", .img = "icons/weapons/swords/sword-jeweled-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Säbel", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Säbel", .img = "icons/weapons/swords/scimitar-worn-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Säbel", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 60, .unit = .unze }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Sklaventod", .img = "icons/weapons/swords/scimitar-guard-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Säbel", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Waqqif", .img = "icons/weapons/daggers/dagger-curved-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 2, .distanzklasse = "H", .kampftalente = "Säbel", .laenge = .{ .value = 45, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 35, .unit = .unze }, .preis = .{ .value = 60, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Wolfsmesser", .img = "icons/weapons/swords/sword-jeweled-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Säbel", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 50, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Schwerter", .entries = &.{
.{ .Bewaffnung = .{ .name = "Amazonensäbel", .img = "icons/weapons/swords/scimitar-worn-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 75, .unit = .unze }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Arbach", .img = "icons/weapons/swords/scimitar-guard.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Barbarenschwert", .img = "icons/weapons/swords/sword-guard-flanged.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 13, .schadensschritte = 2, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 4, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Bastardschwert", .img = "icons/weapons/swords/greatsword-crossguard-barbed.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 110, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Breitschwert", .img = "icons/weapons/swords/shortsword-guard-brass.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 85, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kurzschwert", .img = "icons/weapons/swords/shortsword-green.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "HN", .kampftalente = "Schwerter", .laenge = .{ .value = 50, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 40, .unit = .unze }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kusliker Säbel", .img = "icons/weapons/swords/sword-guard-red-jewel.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 80, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 160, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Langschwert", .img = "icons/weapons/swords/sword-guard-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 95, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Nachtwind", .img = "icons/weapons/swords/sword-katana.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 11, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 2, .bruchfaktor = 0, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 500, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Rapier", .img = "icons/weapons/swords/sword-guard-red-jewel.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative =1, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 45, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Robbentöter", .img = "icons/weapons/swords/sword-jeweled-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Säbel", .img = "icons/weapons/swords/scimitar-worn-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 60, .unit = .unze }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
// .{ .Bewaffnung = .{ .name = "Turnierschwert", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3(A)", .schwellenwert = 11, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 80, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 60, .preis = 80 } } },
.{ .Bewaffnung = .{ .name = "Wolfsmesser", .img = "icons/weapons/swords/sword-jeweled-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Schwerter", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 50, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Speere", .entries = &.{
.{ .Bewaffnung = .{ .name = "Drachentöter", .img = "icons/weapons/polearms/spear-barbed-silver.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "3d6+5", .schwellenwert = 20, .schadensschritte = 1, .modifikator_attacke = -2, .modifikator_parade = -4, .initiative = -3, .bruchfaktor = 3, .distanzklasse = "P", .kampftalente = "Speere", .laenge = .{ .value = 400, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 400, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Dreizack", .img = "icons/weapons/polearms/trident-silver-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Speere", .laenge = .{ .value = 140, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Dschadra", .img = "icons/weapons/polearms/spear-flared-purple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -3, .initiative = -1, .bruchfaktor = 6, .distanzklasse = "S", .kampftalente = "Speere", .laenge = .{ .value = 200, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Efferdbart", .img = "icons/weapons/polearms/trident-silver-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+4", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "NS", .kampftalente = "Speere", .laenge = .{ .value = 120, .unit = .halbfinger }, .zweihaendig = false, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Holzspeer", .img = "icons/weapons/polearms/spear-simple-engraved.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = 0, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Speere", .laenge = .{ .value = 150, .unit = .halbfinger }, .zweihaendig = true, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 60, .unit = .unze }, .preis = .{ .value = 10, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Jagdspieß", .img = "icons/weapons/polearms/pike-flared-brown.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+6", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 3, .distanzklasse = "S", .kampftalente = "Speere", .laenge = .{ .value = 200, .unit = .halbfinger }, .zweihaendig = true, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Korspieß", .img = "icons/weapons/staves/staff-hooked-banded.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+2", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "S", .kampftalente = "Speere", .laenge = .{ .value = 180, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = .{ .value = 140, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kriegslanze", .img = "icons/weapons/polearms/spear-flared-steel.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -2, .modifikator_parade = -4, .initiative = -2, .bruchfaktor = 5, .distanzklasse = "P", .kampftalente = "Speere", .laenge = .{ .value = 300, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Partisane", .img = "icons/weapons/polearms/pike-flared-brown.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = 0, .bruchfaktor = 4, .distanzklasse = "S", .kampftalente = "Speere", .laenge = .{ .value = 200, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Pike", .img = "icons/weapons/polearms/spear-flared-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 14, .schadensschritte = 4, .modifikator_attacke = -1, .modifikator_parade = -2, .initiative = -2, .bruchfaktor = 6, .distanzklasse = "P", .kampftalente = "Speere", .laenge = .{ .value = 350, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 180, .unit = .unze }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Schnitter", .img = "icons/weapons/polearms/glaive-simple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 14, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 4, .distanzklasse = "NS", .kampftalente = "Speere", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Speer", .img = "icons/weapons/polearms/spear-flared-worn-grey.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Speere", .laenge = .{ .value = 190, .unit = .halbfinger }, .zweihaendig = true, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 30, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Stoßspeer", .img = "icons/weapons/polearms/pike-flared-brown.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+2", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 3, .distanzklasse = "S", .kampftalente = "Speere", .laenge = .{ .value = 200, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 100, .unit = .silbertaler } } } },
// .{ .Bewaffnung = .{ .name = "Turnierlanze", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2 (A)", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -2, .modifikator_parade = -4, .initiative = -2, .bruchfaktor = 8, .distanzklasse = "P", .kampftalente = "Speere", .laenge = .{ .value = 300, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 120, .preis = 50 } } },
.{ .Bewaffnung = .{ .name = "Wurfspeer", .img = "icons/weapons/polearms/javelin.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 11, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 4, .distanzklasse = "N", .kampftalente = "Speere", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 30, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Wurmspieß", .img = "icons/weapons/polearms/pike-flared-brown.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 13, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = 0, .bruchfaktor = 2, .distanzklasse = "S", .kampftalente = "Speere", .laenge = .{ .value = 180, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Stäbe", .entries = &.{
.{ .Bewaffnung = .{ .name = "Kampfstab", .img = "icons/weapons/staves/staff-simple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 5, .distanzklasse = "NS", .kampftalente = "Stäbe", .laenge = .{ .value = 150, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 40, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Magierstab", .img = "icons/weapons/staves/staff-ornate.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 11, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 0, .distanzklasse = "NS", .kampftalente = "Stäbe", .laenge = .{ .value = 150, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Magierstab mit Kristallkugel", .img = "icons/weapons/staves/staff-ornate-engraved-blue.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = -1, .modifikator_parade = -2, .initiative = -2, .bruchfaktor = 0, .distanzklasse = "N", .kampftalente = "Stäbe", .laenge = .{ .value = 150, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Zweililien", .img = "icons/weapons/polearms/spear-hooked-brown.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 1, .modifikator_parade = -1, .initiative = 1, .bruchfaktor = 4, .distanzklasse = "N", .kampftalente = "Stäbe", .laenge = .{ .value = 140, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 80, .unit = .unze }, .preis = .{ .value = 200, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Zweihandflegel", .entries = &.{
.{ .Bewaffnung = .{ .name = "Dreschflegel", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = -2, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 6, .distanzklasse = "S", .kampftalente = "Zweihandflegel", .laenge = .{ .value = 150, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 15, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kriegsflegel", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+6", .schwellenwert = 12, .schadensschritte = 2, .modifikator_attacke = -1, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Zweihandflegel", .laenge = .{ .value = 150, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 50, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Zweihand-Hiebwaffen", .entries = &.{
.{ .Bewaffnung = .{ .name = "Barbarenstreitaxt", .img = "icons/weapons/axes/axe-double.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "3d6+2", .schwellenwert = 15, .schadensschritte = 1, .modifikator_attacke = -1, .modifikator_parade = -4, .initiative = -2, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 120, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 250, .unit = .unze }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Echsische Axt", .img = "icons/weapons/axes/axe-double-jagged-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "NS", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 150, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Felsspalter", .img = "icons/weapons/axes/axe-double-engraved-runes.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+2", .schwellenwert = 14, .schadensschritte = 2, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 120, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 300, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Gruufhai", .img = "icons/weapons/hammers/hammer-drilling-spiked.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+6", .schwellenwert = 14, .schadensschritte = 2, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 3, .distanzklasse = "N", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 120, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 180, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Holzfälleraxt", .img = "icons/weapons/axes/axe-broad-simple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6", .schwellenwert = 12, .schadensschritte = 2, .modifikator_attacke = -1, .modifikator_parade = -4, .initiative = -2, .bruchfaktor = 5, .distanzklasse = "N", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 110, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 160, .unit = .unze }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Kriegshammer", .img = "icons/weapons/hammers/hammer-war-spiked.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+3", .schwellenwert = 14, .schadensschritte = 2, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 2, .distanzklasse = "N", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 180, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Neethaner Langaxt", .img = "icons/weapons/polearms/glaive-hooked-steel.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+2", .schwellenwert = 13, .schadensschritte = 4, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 5, .distanzklasse = "S", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 180, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 160, .unit = .unze }, .preis = .{ .value = 160, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Orknase", .img = "icons/weapons/polearms/halberd-engraved-steel.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 12, .schadensschritte = 2, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 4, .distanzklasse = "N", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 110, .unit = .unze }, .preis = .{ .value = 75, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Pailos", .img = "icons/weapons/polearms/halberd-crescent-wood.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+4", .schwellenwert = 14, .schadensschritte = 2, .modifikator_attacke = -1, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 3, .distanzklasse = "S", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 175, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 180, .unit = .unze }, .preis = .{ .value = 300, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Schnitter", .img = "icons/weapons/polearms/glaive-simple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 14, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 4, .distanzklasse = "NS", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 90, .unit = .unze }, .preis = .{ .value = 120, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Spitzhacke", .img = "icons/tools/hand/pickaxe-steel-white.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+6", .schwellenwert = 13, .schadensschritte = 2, .modifikator_attacke = -2, .modifikator_parade = -4, .initiative = -3, .bruchfaktor = 5, .distanzklasse = "N", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 100, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 200, .unit = .unze }, .preis = .{ .value = 20, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Vorschlaghammer", .img = "icons/weapons/hammers/hammer-double-stone-worn.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 13, .schadensschritte = 2, .modifikator_attacke = -2, .modifikator_parade = -4, .initiative = -3, .bruchfaktor = 5, .distanzklasse = "N", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 90, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = .{ .value = 250, .unit = .unze }, .preis = .{ .value = 30, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Warunker Hammer", .img = "icons/weapons/hammers/hammer-war-rounding.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+6", .schwellenwert = 14, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "NS", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 150, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Zwergenschlägel", .img = "icons/weapons/hammers/hammer-double-engraved-gold.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 1, .distanzklasse = "N", .kampftalente = "Zweihand-Hiebwaffen", .laenge = .{ .value = 120, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 150, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Zweihandschwerter/-säbel", .entries = &.{
.{ .Bewaffnung = .{ .name = "Andergaster", .img = "icons/weapons/swords/greatsword-guard-jewel-green.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "3d6+2", .schwellenwert = 14, .schadensschritte = 2, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -3, .bruchfaktor = 3, .distanzklasse = "S", .kampftalente = "Zweihandschwerter/-säbel", .laenge = .{ .value = 200, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 220, .unit = .unze }, .preis = .{ .value = 350, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Anderthalbhänder", .img = "icons/weapons/swords/greatsword-crossguard-engraved-green.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+5", .schwellenwert = 11, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 1, .distanzklasse = "NS", .kampftalente = "Zweihandschwerter/-säbel", .laenge = .{ .value = 115, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Boronssichel", .img = "icons/weapons/swords/sword-katana-purple.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+6", .schwellenwert = 13, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -3, .initiative = -2, .bruchfaktor = 3, .distanzklasse = "S", .kampftalente = "Zweihandschwerter/-säbel", .laenge = .{ .value = 180, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 160, .unit = .unze }, .preis = .{ .value = 400, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Doppelkhunchomer", .img = "icons/weapons/swords/scimitar-guard-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+6", .schwellenwert = 12, .schadensschritte = 2, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "NS", .kampftalente = "Zweihandschwerter/-säbel", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 150, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Großer Sklaventod", .img = "icons/weapons/swords/scimitar-guard-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+4", .schwellenwert = 13, .schadensschritte = 2, .modifikator_attacke = 0, .modifikator_parade = -2, .initiative = -2, .bruchfaktor = 3, .distanzklasse = "NS", .kampftalente = "Zweihandschwerter/-säbel", .laenge = .{ .value = 140, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 160, .unit = .unze }, .preis = .{ .value = 350, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Richtschwert", .img = "icons/weapons/swords/sword-guard-red.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "3d6+4", .schwellenwert = 13, .schadensschritte = 2, .modifikator_attacke = -2, .modifikator_parade = -4, .initiative = -3, .bruchfaktor = 5, .distanzklasse = "N", .kampftalente = "Zweihandschwerter/-säbel", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = true }, .gewicht = .{ .value = 200, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Rondrakamm", .img = "icons/weapons/swords/greatsword-flamberge.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+2", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 0, .bruchfaktor = 1, .distanzklasse = "NS", .kampftalente = "Zweihandschwerter/-säbel", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = .{ .value = 130, .unit = .unze }, .preis = .{ .value = 0, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Tuzakmesser", .img = "icons/weapons/swords/sword-katana.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+6", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = 1, .bruchfaktor = 1, .distanzklasse = "NS", .kampftalente = "Zweihandschwerter/-säbel", .laenge = .{ .value = 130, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 100, .unit = .unze }, .preis = .{ .value = 400, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Zweihänder", .img = "icons/weapons/swords/greatsword-crossguard-silver.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "2d6+4", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 2, .distanzklasse = "NS", .kampftalente = "Zweihandschwerter/-säbel", .laenge = .{ .value = 155, .unit = .halbfinger }, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = .{ .value = 160, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
}}},
.{ .Folder = .{ .name = "Handgemenge-Waffen (Raufen)", .entries = &.{
// .{ .Bewaffnung = .{ .name = "Fausthieb", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6 (A)*", .schwellenwert = 10, .schadensschritte = 3, .modifikator_attacke = -1, .modifikator_parade = -2**, .initiative = -2, .bruchfaktor = -, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = -, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = -, .preis = - } } },
// .{ .Bewaffnung = .{ .name = "Tritt/Kopfstoß", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6 (A)*", .schwellenwert = 10, .schadensschritte = 3, .modifikator_attacke = -1, .modifikator_parade = -2**, .initiative = -1, .bruchfaktor = -, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = -, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = -, .preis = - } } },
.{ .Bewaffnung = .{ .name = "Drachenklaue", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = -1, .bruchfaktor = 0, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = 20, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 200, .unit = .unze }, .preis = .{ .value = 350, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Drachenklaue (lange Klinge)", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+3", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = -1, .bruchfaktor = 1, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = 30, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 200, .unit = .unze }, .preis = .{ .value = 390, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Drachenklaue (Klingenfänger)", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = -1, .bruchfaktor = 0, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = 20, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 200, .unit = .unze }, .preis = .{ .value = 390, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Drachenklaue (Klingenbrecher)", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = -1, .bruchfaktor = 0, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = 20, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 200, .unit = .unze }, .preis = .{ .value = 410, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Veteranenhand", .img = "icons/weapons/fist/claw-gauntlet-gray.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 12, .schadensschritte = 4, .modifikator_attacke = 0, .modifikator_parade = -1, .initiative = -1, .bruchfaktor = 4, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = 0, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 70, .unit = .unze }, .preis = .{ .value = 250, .unit = .silbertaler } } } },
// .{ .Bewaffnung = .{ .name = "Schlagring", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2 (A)", .schwellenwert = 10, .schadensschritte = 3, .modifikator_attacke = -1, .modifikator_parade = -2, .initiative = 0, .bruchfaktor = 0, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = -, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 20, .preis = 25 } } },
.{ .Bewaffnung = .{ .name = "Orchidee", .img = "icons/weapons/fist/claw-cloth-brown.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+1", .schwellenwert = 12, .schadensschritte = 5, .modifikator_attacke = -1, .modifikator_parade = -2, .initiative = 0, .bruchfaktor = 3, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = 0, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 35, .unit = .unze }, .preis = .{ .value = 180, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Panzerarm", .img = "icons/weapons/fist/glove-spiked.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 11, .schadensschritte = 3, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = -1, .bruchfaktor = -2, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = 20, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 220, .unit = .unze }, .preis = .{ .value = 140, .unit = .silbertaler } } } },
.{ .Bewaffnung = .{ .name = "Bock", .img = "icons/weapons/fist/fist-katar-triple-gold-black.webp", .system = .{ .nahkampfwaffe = .{ .aktiv = true, .basis = "1d6+2", .schwellenwert = 10, .schadensschritte = 5, .modifikator_attacke = 0, .modifikator_parade = 0, .initiative = -1, .bruchfaktor = 0, .distanzklasse = "H", .kampftalente = "Raufen", .laenge = .{ .value = 20, .unit = .halbfinger }, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = .{ .value = 120, .unit = .unze }, .preis = .{ .value = 80, .unit = .silbertaler } } } },
}}},
}};
pub fn main() !void {
const progress = std.Progress.start(.{ .root_name = "Building compendiums", .estimated_total_items = 3 });
defer progress.end();
try std.fs.cwd().makePath("packs");
try talente .serialize("packs/talente"); progress.completeOne();
try ruestungen .serialize("packs/ruestungen"); progress.completeOne();
try bewaffnungen.serialize("packs/bewaffnungen"); progress.completeOne();
}
fn print_contents(path: [:0]const u8) !void {
var diagnostic: leveldb.Diagnostic = null;
const db = leveldb.open(.{ .path = path, .diagnostic = &diagnostic }) catch |err| {
std.log.err("leveldb.open failed: {s}", .{ diagnostic.? });
return err;
};
defer db.close();
const iter = db.iterator(.{});
defer iter.destroy();
iter.seek_to_first();
while(iter.is_valid()) {
defer iter.next();
std.log.debug("key: '{s}' value: '{s}'", .{ iter.key(), iter.value() });
}
}
+164
View File
@@ -0,0 +1,164 @@
const std = @import("std");
const leveldb = @import("leveldb");
const system = @import("system.zig");
const String = []const u8;
const CORE_VERSION = "12.331";
const SYSTEM_NAME = system.SYSTEM_NAME;
const SYSTEM_VERSION = system.SYSTEM_VERSION;
// Entry must be a tagged union with one tag being
// Folder: struct {
// name: String,
// entries: []const Entry,
// },
// and the other tags being the respective Foundry type name
pub fn Compendium(base_type: BaseType, Entry: type) type {
if (std.meta.activeTag(@typeInfo(Entry)) != .Union) @compileError("Entry must be a tagged union.");
if (@typeInfo(Entry).Union.tag_type == null) @compileError("Entry must be a tagged union.");
return struct {
entries: []const Entry = &.{},
pub fn serialize(self: @This(), path: [:0]const u8) !void {
try leveldb.destroy(.{ .path = path });
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
var diagnostic: leveldb.Diagnostic = null;
const db = leveldb.open(.{ .path = path, .diagnostic = &diagnostic, .options = .{ .create_if_missing = true, .compression = .Snappy } }) catch |err| {
std.log.err("leveldb.open failed: {s}", .{ diagnostic.? });
return err;
};
defer db.close();
for (self.entries) |entry| {
defer _ = arena.reset(.retain_capacity);
try serialize_entry(arena.allocator(), db, entry, null);
}
}
fn serialize_entry(allocator: std.mem.Allocator, db: leveldb, entry: Entry, folder: ?String) !void{
switch (entry) {
.Folder => |_folder| {
const foundry_folder: Folder(base_type) = .{
.name = _folder.name,
._id = &random_id(),
.folder = folder,
};
const key = try std.fmt.allocPrintZ(allocator, "!folders!{s}", .{ foundry_folder._id.? });
const value = try std.json.stringifyAlloc(allocator, foundry_folder, .{});
try db.put(.{ .key = key, .value = value });
for (_folder.entries) |folder_entry| {
try serialize_entry(allocator, db, folder_entry, foundry_folder._id);
}
},
inline else => |item| {
std.debug.assert(item.folder == null);
var foundry_item = item;
foundry_item.folder = folder;
if (foundry_item._id == null)
foundry_item._id = &random_id();
const key = try std.fmt.allocPrintZ(allocator, "!{s}!{s}", .{ base_type.to_compendium_type(), foundry_item._id.? });
const value = try std.json.stringifyAlloc(allocator, foundry_item, .{});
try db.put(.{ .key = key, .value = value });
}
}
}
};
}
const BaseType = enum {
Item,
Actor,
fn to_compendium_type(self: @This()) String {
return switch (self) {
.Item => "items",
.Actor => "actors",
};
}
};
fn Folder(base_type: BaseType) type {
return struct {
_id: ?String = null,
name: String,
@"type": BaseType = base_type,
description: String = "",
folder: ?String = null,
sorting: enum { a, m } = .a,
sort: u64 = 0,
color: ?String = null,
flags: struct {} = .{},
_stats: DocumentStats = .{},
};
}
pub fn Item(comptime typename: String, comptime T: type) type {
return struct {
const Type: BaseType = .Item;
_id: ?String = null,
name: String,
@"type": String = typename,
img: String = "icons/svg/item-bag.svg",
system: T,
effects: []u0 = &.{},
folder: ?String = null,
sort: u64 = 0,
ownership: struct {
default: u8 = 0,
} = .{},
flags: struct {} = .{},
_stats: DocumentStats = .{},
};
}
pub const DocumentStats = struct {
coreVersion: String = CORE_VERSION,
systemId: String = SYSTEM_NAME,
systemVersion: String = SYSTEM_VERSION,
createdTime: ?u64 = null,
modifiedTime: ?u64 = null,
lastModifiedBy: ?String = null,
compendiumSource: ?String = null,
duplicateSource: ?String = null,
};
inline fn random_char(alphabet: []const u8) u8 {
return alphabet[std.crypto.random.uintLessThan(u8, alphabet.len)];
}
var id_set = std.BufSet.init(std.heap.c_allocator);
fn random_id() [16]u8 {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var result: [16]u8 = undefined;
inline for (&result) |*c| c.* = random_char(alphabet);
if (id_set.contains(&result)) {
return random_id();
}
id_set.insert(&result) catch unreachable;
return result;
}
+81
View File
@@ -0,0 +1,81 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions (.{});
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });
const leveldb_static_lib = build_leveldb(b, target, optimize);
const module = b.addModule("leveldb", .{
.root_source_file = b.path("src/leveldb.zig"),
.target = target,
.optimize = optimize,
});
module.linkLibrary(leveldb_static_lib);
}
fn build_leveldb(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile {
const source = b.dependency("leveldb", .{});
const static_lib = b.addStaticLibrary(.{
.name = "leveldb",
.target = target,
.optimize = optimize,
});
static_lib.linkLibCpp();
static_lib.addIncludePath(source.path(""));
static_lib.addIncludePath(source.path("include"));
static_lib.installHeadersDirectory(source.path("include"), "", .{});
if (target.result.os.tag == .windows) {
static_lib.defineCMacro("LEVELDB_PLATFORM_WINDOWS", "1");
static_lib.addCSourceFile(.{ .file = source.path("util/env_windows.cc") });
} else {
static_lib.defineCMacro("LEVELDB_PLATFORM_POSIX", "1");
static_lib.addCSourceFile(.{ .file = source.path("util/env_posix.cc") });
}
static_lib.addCSourceFiles(.{
.root = source.path(""),
.files = &.{
"db/builder.cc",
"db/c.cc",
"db/db_impl.cc",
"db/db_iter.cc",
"db/dbformat.cc",
"db/dumpfile.cc",
"db/filename.cc",
"db/log_reader.cc",
"db/log_writer.cc",
"db/memtable.cc",
"db/repair.cc",
"db/table_cache.cc",
"db/version_edit.cc",
"db/version_set.cc",
"db/write_batch.cc",
"table/block_builder.cc",
"table/block.cc",
"table/filter_block.cc",
"table/format.cc",
"table/iterator.cc",
"table/merger.cc",
"table/table_builder.cc",
"table/table.cc",
"table/two_level_iterator.cc",
"util/arena.cc",
"util/bloom.cc",
"util/cache.cc",
"util/coding.cc",
"util/comparator.cc",
"util/crc32c.cc",
"util/env.cc",
"util/filter_policy.cc",
"util/hash.cc",
"util/logging.cc",
"util/options.cc",
"util/status.cc",
},
});
b.installArtifact(static_lib);
return static_lib;
}
+15
View File
@@ -0,0 +1,15 @@
.{
.name = "leveldb",
.version = "1.0.0",
.paths = .{
"build.zig",
"build.zig.zon",
"leveldb.zig",
},
.dependencies = .{
.leveldb = .{
.path = "libs/leveldb"
},
},
}
@@ -0,0 +1,18 @@
# Run manually to reformat a file:
# clang-format -i --style=file <file>
# find . -iname '*.cc' -o -iname '*.h' -o -iname '*.h.in' | xargs clang-format -i --style=file
BasedOnStyle: Google
DerivePointerAlignment: false
# Public headers are in a different location in the internal Google repository.
# Order them so that when imported to the authoritative repository they will be
# in correct alphabetical order.
IncludeCategories:
- Regex: '^(<|"(benchmarks|db|helpers)/)'
Priority: 1
- Regex: '^"(leveldb)/'
Priority: 2
- Regex: '^(<|"(issues|port|table|third_party|util)/)'
Priority: 3
- Regex: '.*'
Priority: 4
@@ -0,0 +1,102 @@
# Copyright 2021 The LevelDB Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. See the AUTHORS file for names of contributors.
name: ci
on: [push, pull_request]
permissions:
contents: read
jobs:
build-and-test:
name: >-
CI
${{ matrix.os }}
${{ matrix.compiler }}
${{ matrix.optimized && 'release' || 'debug' }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
compiler: [clang, gcc, msvc]
os: [ubuntu-latest, macos-latest, windows-latest]
optimized: [true, false]
exclude:
# MSVC only works on Windows.
- os: ubuntu-latest
compiler: msvc
- os: macos-latest
compiler: msvc
# Not testing with GCC on macOS.
- os: macos-latest
compiler: gcc
# Only testing with MSVC on Windows.
- os: windows-latest
compiler: clang
- os: windows-latest
compiler: gcc
include:
- compiler: clang
CC: clang
CXX: clang++
- compiler: gcc
CC: gcc
CXX: g++
- compiler: msvc
CC:
CXX:
env:
CMAKE_BUILD_DIR: ${{ github.workspace }}/build
CMAKE_BUILD_TYPE: ${{ matrix.optimized && 'RelWithDebInfo' || 'Debug' }}
CC: ${{ matrix.CC }}
CXX: ${{ matrix.CXX }}
BINARY_SUFFIX: ${{ startsWith(matrix.os, 'windows') && '.exe' || '' }}
BINARY_PATH: >-
${{ format(
startsWith(matrix.os, 'windows') && '{0}\build\{1}\' || '{0}/build/',
github.workspace,
matrix.optimized && 'RelWithDebInfo' || 'Debug') }}
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Install dependencies on Linux
if: ${{ runner.os == 'Linux' }}
# libgoogle-perftools-dev is temporarily removed from the package list
# because it is currently broken on GitHub's Ubuntu 22.04.
run: |
sudo apt-get update
sudo apt-get install libkyotocabinet-dev libsnappy-dev libsqlite3-dev
- name: Generate build config
run: >-
cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}"
-DCMAKE_BUILD_TYPE=${{ env.CMAKE_BUILD_TYPE }}
-DCMAKE_INSTALL_PREFIX=${{ runner.temp }}/install_test/
- name: Build
run: >-
cmake --build "${{ env.CMAKE_BUILD_DIR }}"
--config "${{ env.CMAKE_BUILD_TYPE }}"
- name: Run Tests
working-directory: ${{ github.workspace }}/build
run: ctest -C "${{ env.CMAKE_BUILD_TYPE }}" --verbose
- name: Run LevelDB Benchmarks
run: ${{ env.BINARY_PATH }}db_bench${{ env.BINARY_SUFFIX }}
- name: Run SQLite Benchmarks
if: ${{ runner.os != 'Windows' }}
run: ${{ env.BINARY_PATH }}db_bench_sqlite3${{ env.BINARY_SUFFIX }}
- name: Run Kyoto Cabinet Benchmarks
if: ${{ runner.os == 'Linux' && matrix.compiler == 'clang' }}
run: ${{ env.BINARY_PATH }}db_bench_tree_db${{ env.BINARY_SUFFIX }}
- name: Test CMake installation
run: cmake --build "${{ env.CMAKE_BUILD_DIR }}" --target install
+8
View File
@@ -0,0 +1,8 @@
# Editors.
*.sw*
.vscode
.DS_Store
# Build directory.
build/
out/
@@ -0,0 +1,6 @@
[submodule "third_party/googletest"]
path = third_party/googletest
url = https://github.com/google/googletest.git
[submodule "third_party/benchmark"]
path = third_party/benchmark
url = https://github.com/google/benchmark
+12
View File
@@ -0,0 +1,12 @@
# Names should be added to this file like so:
# Name or Organization <email address>
Google Inc.
# Initial version authors:
Jeffrey Dean <jeff@google.com>
Sanjay Ghemawat <sanjay@google.com>
# Partial list of contributors:
Kevin Regan <kevin.d.regan@gmail.com>
Johan Bilien <jobi@litl.com>
@@ -0,0 +1,519 @@
# Copyright 2017 The LevelDB Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. See the AUTHORS file for names of contributors.
cmake_minimum_required(VERSION 3.9)
# Keep the version below in sync with the one in db.h
project(leveldb VERSION 1.23.0 LANGUAGES C CXX)
# C standard can be overridden when this is used as a sub-project.
if(NOT CMAKE_C_STANDARD)
# This project can use C11, but will gracefully decay down to C89.
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED OFF)
set(CMAKE_C_EXTENSIONS OFF)
endif(NOT CMAKE_C_STANDARD)
# C++ standard can be overridden when this is used as a sub-project.
if(NOT CMAKE_CXX_STANDARD)
# This project requires C++11.
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
endif(NOT CMAKE_CXX_STANDARD)
if (WIN32)
set(LEVELDB_PLATFORM_NAME LEVELDB_PLATFORM_WINDOWS)
# TODO(cmumford): Make UNICODE configurable for Windows.
add_definitions(-D_UNICODE -DUNICODE)
else (WIN32)
set(LEVELDB_PLATFORM_NAME LEVELDB_PLATFORM_POSIX)
endif (WIN32)
option(LEVELDB_BUILD_TESTS "Build LevelDB's unit tests" ON)
option(LEVELDB_BUILD_BENCHMARKS "Build LevelDB's benchmarks" ON)
option(LEVELDB_INSTALL "Install LevelDB's header and library" ON)
include(CheckIncludeFile)
check_include_file("unistd.h" HAVE_UNISTD_H)
include(CheckLibraryExists)
check_library_exists(crc32c crc32c_value "" HAVE_CRC32C)
check_library_exists(snappy snappy_compress "" HAVE_SNAPPY)
check_library_exists(zstd zstd_compress "" HAVE_ZSTD)
check_library_exists(tcmalloc malloc "" HAVE_TCMALLOC)
include(CheckCXXSymbolExists)
# Using check_cxx_symbol_exists() instead of check_c_symbol_exists() because
# we're including the header from C++, and feature detection should use the same
# compiler language that the project will use later. Principles aside, some
# versions of do not expose fdatasync() in <unistd.h> in standard C mode
# (-std=c11), but do expose the function in standard C++ mode (-std=c++11).
check_cxx_symbol_exists(fdatasync "unistd.h" HAVE_FDATASYNC)
check_cxx_symbol_exists(F_FULLFSYNC "fcntl.h" HAVE_FULLFSYNC)
check_cxx_symbol_exists(O_CLOEXEC "fcntl.h" HAVE_O_CLOEXEC)
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
# Disable C++ exceptions.
string(REGEX REPLACE "/EH[a-z]+" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHs-c-")
add_definitions(-D_HAS_EXCEPTIONS=0)
# Disable RTTI.
string(REGEX REPLACE "/GR" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GR-")
else(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
# Enable strict prototype warnings for C code in clang and gcc.
if(NOT CMAKE_C_FLAGS MATCHES "-Wstrict-prototypes")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wstrict-prototypes")
endif(NOT CMAKE_C_FLAGS MATCHES "-Wstrict-prototypes")
# Disable C++ exceptions.
string(REGEX REPLACE "-fexceptions" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
# Disable RTTI.
string(REGEX REPLACE "-frtti" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
endif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
# Test whether -Wthread-safety is available. See
# https://clang.llvm.org/docs/ThreadSafetyAnalysis.html
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag(-Wthread-safety HAVE_CLANG_THREAD_SAFETY)
# Used by googletest.
check_cxx_compiler_flag(-Wno-missing-field-initializers
LEVELDB_HAVE_NO_MISSING_FIELD_INITIALIZERS)
include(CheckCXXSourceCompiles)
# Test whether C++17 __has_include is available.
check_cxx_source_compiles("
#if defined(__has_include) && __has_include(<string>)
#include <string>
#endif
int main() { std::string str; return 0; }
" HAVE_CXX17_HAS_INCLUDE)
set(LEVELDB_PUBLIC_INCLUDE_DIR "include/leveldb")
set(LEVELDB_PORT_CONFIG_DIR "include/port")
configure_file(
"port/port_config.h.in"
"${PROJECT_BINARY_DIR}/${LEVELDB_PORT_CONFIG_DIR}/port_config.h"
)
include_directories(
"${PROJECT_BINARY_DIR}/include"
"."
)
if(BUILD_SHARED_LIBS)
# Only export LEVELDB_EXPORT symbols from the shared library.
add_compile_options(-fvisibility=hidden)
endif(BUILD_SHARED_LIBS)
# Must be included before CMAKE_INSTALL_INCLUDEDIR is used.
include(GNUInstallDirs)
add_library(leveldb "")
target_sources(leveldb
PRIVATE
"${PROJECT_BINARY_DIR}/${LEVELDB_PORT_CONFIG_DIR}/port_config.h"
"db/builder.cc"
"db/builder.h"
"db/c.cc"
"db/db_impl.cc"
"db/db_impl.h"
"db/db_iter.cc"
"db/db_iter.h"
"db/dbformat.cc"
"db/dbformat.h"
"db/dumpfile.cc"
"db/filename.cc"
"db/filename.h"
"db/log_format.h"
"db/log_reader.cc"
"db/log_reader.h"
"db/log_writer.cc"
"db/log_writer.h"
"db/memtable.cc"
"db/memtable.h"
"db/repair.cc"
"db/skiplist.h"
"db/snapshot.h"
"db/table_cache.cc"
"db/table_cache.h"
"db/version_edit.cc"
"db/version_edit.h"
"db/version_set.cc"
"db/version_set.h"
"db/write_batch_internal.h"
"db/write_batch.cc"
"port/port_stdcxx.h"
"port/port.h"
"port/thread_annotations.h"
"table/block_builder.cc"
"table/block_builder.h"
"table/block.cc"
"table/block.h"
"table/filter_block.cc"
"table/filter_block.h"
"table/format.cc"
"table/format.h"
"table/iterator_wrapper.h"
"table/iterator.cc"
"table/merger.cc"
"table/merger.h"
"table/table_builder.cc"
"table/table.cc"
"table/two_level_iterator.cc"
"table/two_level_iterator.h"
"util/arena.cc"
"util/arena.h"
"util/bloom.cc"
"util/cache.cc"
"util/coding.cc"
"util/coding.h"
"util/comparator.cc"
"util/crc32c.cc"
"util/crc32c.h"
"util/env.cc"
"util/filter_policy.cc"
"util/hash.cc"
"util/hash.h"
"util/logging.cc"
"util/logging.h"
"util/mutexlock.h"
"util/no_destructor.h"
"util/options.cc"
"util/random.h"
"util/status.cc"
# Only CMake 3.3+ supports PUBLIC sources in targets exported by "install".
$<$<VERSION_GREATER:CMAKE_VERSION,3.2>:PUBLIC>
"${LEVELDB_PUBLIC_INCLUDE_DIR}/c.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/cache.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/comparator.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/db.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/dumpfile.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/env.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/export.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/filter_policy.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/iterator.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/options.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/slice.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/status.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/table_builder.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/table.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/write_batch.h"
)
if (WIN32)
target_sources(leveldb
PRIVATE
"util/env_windows.cc"
"util/windows_logger.h"
)
else (WIN32)
target_sources(leveldb
PRIVATE
"util/env_posix.cc"
"util/posix_logger.h"
)
endif (WIN32)
# MemEnv is not part of the interface and could be pulled to a separate library.
target_sources(leveldb
PRIVATE
"helpers/memenv/memenv.cc"
"helpers/memenv/memenv.h"
)
target_include_directories(leveldb
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
set_target_properties(leveldb
PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR})
target_compile_definitions(leveldb
PRIVATE
# Used by include/export.h when building shared libraries.
LEVELDB_COMPILE_LIBRARY
# Used by port/port.h.
${LEVELDB_PLATFORM_NAME}=1
)
if (NOT HAVE_CXX17_HAS_INCLUDE)
target_compile_definitions(leveldb
PRIVATE
LEVELDB_HAS_PORT_CONFIG_H=1
)
endif(NOT HAVE_CXX17_HAS_INCLUDE)
if(BUILD_SHARED_LIBS)
target_compile_definitions(leveldb
PUBLIC
# Used by include/export.h.
LEVELDB_SHARED_LIBRARY
)
endif(BUILD_SHARED_LIBS)
if(HAVE_CLANG_THREAD_SAFETY)
target_compile_options(leveldb
PUBLIC
-Werror -Wthread-safety)
endif(HAVE_CLANG_THREAD_SAFETY)
if(HAVE_CRC32C)
target_link_libraries(leveldb crc32c)
endif(HAVE_CRC32C)
if(HAVE_SNAPPY)
target_link_libraries(leveldb snappy)
endif(HAVE_SNAPPY)
if(HAVE_ZSTD)
target_link_libraries(leveldb zstd)
endif(HAVE_ZSTD)
if(HAVE_TCMALLOC)
target_link_libraries(leveldb tcmalloc)
endif(HAVE_TCMALLOC)
# Needed by port_stdcxx.h
find_package(Threads REQUIRED)
target_link_libraries(leveldb Threads::Threads)
add_executable(leveldbutil
"db/leveldbutil.cc"
)
target_link_libraries(leveldbutil leveldb)
if(LEVELDB_BUILD_TESTS)
enable_testing()
# Prevent overriding the parent project's compiler/linker settings on Windows.
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(install_gtest OFF)
set(install_gmock OFF)
set(build_gmock ON)
# This project is tested using GoogleTest.
add_subdirectory("third_party/googletest")
# GoogleTest triggers a missing field initializers warning.
if(LEVELDB_HAVE_NO_MISSING_FIELD_INITIALIZERS)
set_property(TARGET gtest
APPEND PROPERTY COMPILE_OPTIONS -Wno-missing-field-initializers)
set_property(TARGET gmock
APPEND PROPERTY COMPILE_OPTIONS -Wno-missing-field-initializers)
endif(LEVELDB_HAVE_NO_MISSING_FIELD_INITIALIZERS)
add_executable(leveldb_tests "")
target_sources(leveldb_tests
PRIVATE
# "db/fault_injection_test.cc"
# "issues/issue178_test.cc"
# "issues/issue200_test.cc"
# "issues/issue320_test.cc"
"${PROJECT_BINARY_DIR}/${LEVELDB_PORT_CONFIG_DIR}/port_config.h"
# "util/env_test.cc"
"util/status_test.cc"
"util/no_destructor_test.cc"
"util/testutil.cc"
"util/testutil.h"
)
if(NOT BUILD_SHARED_LIBS)
target_sources(leveldb_tests
PRIVATE
"db/autocompact_test.cc"
"db/corruption_test.cc"
"db/db_test.cc"
"db/dbformat_test.cc"
"db/filename_test.cc"
"db/log_test.cc"
"db/recovery_test.cc"
"db/skiplist_test.cc"
"db/version_edit_test.cc"
"db/version_set_test.cc"
"db/write_batch_test.cc"
"helpers/memenv/memenv_test.cc"
"table/filter_block_test.cc"
"table/table_test.cc"
"util/arena_test.cc"
"util/bloom_test.cc"
"util/cache_test.cc"
"util/coding_test.cc"
"util/crc32c_test.cc"
"util/hash_test.cc"
"util/logging_test.cc"
)
endif(NOT BUILD_SHARED_LIBS)
target_link_libraries(leveldb_tests leveldb gmock gtest gtest_main)
target_compile_definitions(leveldb_tests
PRIVATE
${LEVELDB_PLATFORM_NAME}=1
)
if (NOT HAVE_CXX17_HAS_INCLUDE)
target_compile_definitions(leveldb_tests
PRIVATE
LEVELDB_HAS_PORT_CONFIG_H=1
)
endif(NOT HAVE_CXX17_HAS_INCLUDE)
add_test(NAME "leveldb_tests" COMMAND "leveldb_tests")
function(leveldb_test test_file)
get_filename_component(test_target_name "${test_file}" NAME_WE)
add_executable("${test_target_name}" "")
target_sources("${test_target_name}"
PRIVATE
"${PROJECT_BINARY_DIR}/${LEVELDB_PORT_CONFIG_DIR}/port_config.h"
"util/testutil.cc"
"util/testutil.h"
"${test_file}"
)
target_link_libraries("${test_target_name}" leveldb gmock gtest)
target_compile_definitions("${test_target_name}"
PRIVATE
${LEVELDB_PLATFORM_NAME}=1
)
if (NOT HAVE_CXX17_HAS_INCLUDE)
target_compile_definitions("${test_target_name}"
PRIVATE
LEVELDB_HAS_PORT_CONFIG_H=1
)
endif(NOT HAVE_CXX17_HAS_INCLUDE)
add_test(NAME "${test_target_name}" COMMAND "${test_target_name}")
endfunction(leveldb_test)
leveldb_test("db/c_test.c")
if(NOT BUILD_SHARED_LIBS)
# TODO(costan): This test also uses
# "util/env_{posix|windows}_test_helper.h"
if (WIN32)
leveldb_test("util/env_windows_test.cc")
else (WIN32)
leveldb_test("util/env_posix_test.cc")
endif (WIN32)
endif(NOT BUILD_SHARED_LIBS)
endif(LEVELDB_BUILD_TESTS)
if(LEVELDB_BUILD_BENCHMARKS)
# This project uses Google benchmark for benchmarking.
set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(BENCHMARK_ENABLE_EXCEPTIONS OFF CACHE BOOL "" FORCE)
add_subdirectory("third_party/benchmark")
function(leveldb_benchmark bench_file)
get_filename_component(bench_target_name "${bench_file}" NAME_WE)
add_executable("${bench_target_name}" "")
target_sources("${bench_target_name}"
PRIVATE
"${PROJECT_BINARY_DIR}/${LEVELDB_PORT_CONFIG_DIR}/port_config.h"
"util/histogram.cc"
"util/histogram.h"
"util/testutil.cc"
"util/testutil.h"
"${bench_file}"
)
target_link_libraries("${bench_target_name}" leveldb gmock gtest benchmark)
target_compile_definitions("${bench_target_name}"
PRIVATE
${LEVELDB_PLATFORM_NAME}=1
)
if (NOT HAVE_CXX17_HAS_INCLUDE)
target_compile_definitions("${bench_target_name}"
PRIVATE
LEVELDB_HAS_PORT_CONFIG_H=1
)
endif(NOT HAVE_CXX17_HAS_INCLUDE)
endfunction(leveldb_benchmark)
if(NOT BUILD_SHARED_LIBS)
leveldb_benchmark("benchmarks/db_bench.cc")
endif(NOT BUILD_SHARED_LIBS)
check_library_exists(sqlite3 sqlite3_open "" HAVE_SQLITE3)
if(HAVE_SQLITE3)
leveldb_benchmark("benchmarks/db_bench_sqlite3.cc")
target_link_libraries(db_bench_sqlite3 sqlite3)
endif(HAVE_SQLITE3)
# check_library_exists is insufficient here because the library names have
# different manglings when compiled with clang or gcc, at least when installed
# with Homebrew on Mac.
set(OLD_CMAKE_REQURED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES})
list(APPEND CMAKE_REQUIRED_LIBRARIES kyotocabinet)
check_cxx_source_compiles("
#include <kcpolydb.h>
int main() {
kyotocabinet::TreeDB* db = new kyotocabinet::TreeDB();
delete db;
return 0;
}
" HAVE_KYOTOCABINET)
set(CMAKE_REQUIRED_LIBRARIES ${OLD_CMAKE_REQURED_LIBRARIES})
if(HAVE_KYOTOCABINET)
leveldb_benchmark("benchmarks/db_bench_tree_db.cc")
target_link_libraries(db_bench_tree_db kyotocabinet)
endif(HAVE_KYOTOCABINET)
endif(LEVELDB_BUILD_BENCHMARKS)
if(LEVELDB_INSTALL)
install(TARGETS leveldb
EXPORT leveldbTargets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(
FILES
"${LEVELDB_PUBLIC_INCLUDE_DIR}/c.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/cache.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/comparator.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/db.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/dumpfile.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/env.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/export.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/filter_policy.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/iterator.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/options.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/slice.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/status.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/table_builder.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/table.h"
"${LEVELDB_PUBLIC_INCLUDE_DIR}/write_batch.h"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/leveldb"
)
include(CMakePackageConfigHelpers)
configure_package_config_file(
"cmake/${PROJECT_NAME}Config.cmake.in"
"${PROJECT_BINARY_DIR}/cmake/${PROJECT_NAME}Config.cmake"
INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}"
)
write_basic_package_version_file(
"${PROJECT_BINARY_DIR}/cmake/${PROJECT_NAME}ConfigVersion.cmake"
COMPATIBILITY SameMajorVersion
)
install(
EXPORT leveldbTargets
NAMESPACE leveldb::
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}"
)
install(
FILES
"${PROJECT_BINARY_DIR}/cmake/${PROJECT_NAME}Config.cmake"
"${PROJECT_BINARY_DIR}/cmake/${PROJECT_NAME}ConfigVersion.cmake"
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}"
)
endif(LEVELDB_INSTALL)
@@ -0,0 +1,31 @@
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code Reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
See [the README](README.md#contributing-to-the-leveldb-project) for areas
where we are likely to accept external contributions.
## Community Guidelines
This project follows [Google's Open Source Community
Guidelines](https://opensource.google/conduct/).
+27
View File
@@ -0,0 +1,27 @@
Copyright (c) 2011 The LevelDB Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+17
View File
@@ -0,0 +1,17 @@
Release 1.2 2011-05-16
----------------------
Fixes for larger databases (tested up to one billion 100-byte entries,
i.e., ~100GB).
(1) Place hard limit on number of level-0 files. This fixes errors
of the form "too many open files".
(2) Fixed memtable management. Before the fix, a heavy write burst
could cause unbounded memory usage.
A fix for a logging bug where the reader would incorrectly complain
about corruption.
Allow public access to WriteBatch contents so that users can easily
wrap a DB.
+246
View File
@@ -0,0 +1,246 @@
LevelDB is a fast key-value storage library written at Google that provides an ordered mapping from string keys to string values.
> **This repository is receiving very limited maintenance. We will only review the following types of changes.**
>
> * Fixes for critical bugs, such as data loss or memory corruption
> * Changes absolutely needed by internally supported leveldb clients. These typically fix breakage introduced by a language/standard library/OS update
[![ci](https://github.com/google/leveldb/actions/workflows/build.yml/badge.svg)](https://github.com/google/leveldb/actions/workflows/build.yml)
Authors: Sanjay Ghemawat (sanjay@google.com) and Jeff Dean (jeff@google.com)
# Features
* Keys and values are arbitrary byte arrays.
* Data is stored sorted by key.
* Callers can provide a custom comparison function to override the sort order.
* The basic operations are `Put(key,value)`, `Get(key)`, `Delete(key)`.
* Multiple changes can be made in one atomic batch.
* Users can create a transient snapshot to get a consistent view of data.
* Forward and backward iteration is supported over the data.
* Data is automatically compressed using the [Snappy compression library](https://google.github.io/snappy/), but [Zstd compression](https://facebook.github.io/zstd/) is also supported.
* External activity (file system operations etc.) is relayed through a virtual interface so users can customize the operating system interactions.
# Documentation
[LevelDB library documentation](https://github.com/google/leveldb/blob/main/doc/index.md) is online and bundled with the source code.
# Limitations
* This is not a SQL database. It does not have a relational data model, it does not support SQL queries, and it has no support for indexes.
* Only a single process (possibly multi-threaded) can access a particular database at a time.
* There is no client-server support builtin to the library. An application that needs such support will have to wrap their own server around the library.
# Getting the Source
```bash
git clone --recurse-submodules https://github.com/google/leveldb.git
```
# Building
This project supports [CMake](https://cmake.org/) out of the box.
### Build for POSIX
Quick start:
```bash
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release .. && cmake --build .
```
### Building for Windows
First generate the Visual Studio 2017 project/solution files:
```cmd
mkdir build
cd build
cmake -G "Visual Studio 15" ..
```
The default default will build for x86. For 64-bit run:
```cmd
cmake -G "Visual Studio 15 Win64" ..
```
To compile the Windows solution from the command-line:
```cmd
devenv /build Debug leveldb.sln
```
or open leveldb.sln in Visual Studio and build from within.
Please see the CMake documentation and `CMakeLists.txt` for more advanced usage.
# Contributing to the leveldb Project
> **This repository is receiving very limited maintenance. We will only review the following types of changes.**
>
> * Bug fixes
> * Changes absolutely needed by internally supported leveldb clients. These typically fix breakage introduced by a language/standard library/OS update
The leveldb project welcomes contributions. leveldb's primary goal is to be
a reliable and fast key/value store. Changes that are in line with the
features/limitations outlined above, and meet the requirements below,
will be considered.
Contribution requirements:
1. **Tested platforms only**. We _generally_ will only accept changes for
platforms that are compiled and tested. This means POSIX (for Linux and
macOS) or Windows. Very small changes will sometimes be accepted, but
consider that more of an exception than the rule.
2. **Stable API**. We strive very hard to maintain a stable API. Changes that
require changes for projects using leveldb _might_ be rejected without
sufficient benefit to the project.
3. **Tests**: All changes must be accompanied by a new (or changed) test, or
a sufficient explanation as to why a new (or changed) test is not required.
4. **Consistent Style**: This project conforms to the
[Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html).
To ensure your changes are properly formatted please run:
```
clang-format -i --style=file <file>
```
We are unlikely to accept contributions to the build configuration files, such
as `CMakeLists.txt`. We are focused on maintaining a build configuration that
allows us to test that the project works in a few supported configurations
inside Google. We are not currently interested in supporting other requirements,
such as different operating systems, compilers, or build systems.
## Submitting a Pull Request
Before any pull request will be accepted the author must first sign a
Contributor License Agreement (CLA) at https://cla.developers.google.com/.
In order to keep the commit timeline linear
[squash](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Squashing-Commits)
your changes down to a single commit and [rebase](https://git-scm.com/docs/git-rebase)
on google/leveldb/main. This keeps the commit timeline linear and more easily sync'ed
with the internal repository at Google. More information at GitHub's
[About Git rebase](https://help.github.com/articles/about-git-rebase/) page.
# Performance
Here is a performance report (with explanations) from the run of the
included db_bench program. The results are somewhat noisy, but should
be enough to get a ballpark performance estimate.
## Setup
We use a database with a million entries. Each entry has a 16 byte
key, and a 100 byte value. Values used by the benchmark compress to
about half their original size.
LevelDB: version 1.1
Date: Sun May 1 12:11:26 2011
CPU: 4 x Intel(R) Core(TM)2 Quad CPU Q6600 @ 2.40GHz
CPUCache: 4096 KB
Keys: 16 bytes each
Values: 100 bytes each (50 bytes after compression)
Entries: 1000000
Raw Size: 110.6 MB (estimated)
File Size: 62.9 MB (estimated)
## Write performance
The "fill" benchmarks create a brand new database, in either
sequential, or random order. The "fillsync" benchmark flushes data
from the operating system to the disk after every operation; the other
write operations leave the data sitting in the operating system buffer
cache for a while. The "overwrite" benchmark does random writes that
update existing keys in the database.
fillseq : 1.765 micros/op; 62.7 MB/s
fillsync : 268.409 micros/op; 0.4 MB/s (10000 ops)
fillrandom : 2.460 micros/op; 45.0 MB/s
overwrite : 2.380 micros/op; 46.5 MB/s
Each "op" above corresponds to a write of a single key/value pair.
I.e., a random write benchmark goes at approximately 400,000 writes per second.
Each "fillsync" operation costs much less (0.3 millisecond)
than a disk seek (typically 10 milliseconds). We suspect that this is
because the hard disk itself is buffering the update in its memory and
responding before the data has been written to the platter. This may
or may not be safe based on whether or not the hard disk has enough
power to save its memory in the event of a power failure.
## Read performance
We list the performance of reading sequentially in both the forward
and reverse direction, and also the performance of a random lookup.
Note that the database created by the benchmark is quite small.
Therefore the report characterizes the performance of leveldb when the
working set fits in memory. The cost of reading a piece of data that
is not present in the operating system buffer cache will be dominated
by the one or two disk seeks needed to fetch the data from disk.
Write performance will be mostly unaffected by whether or not the
working set fits in memory.
readrandom : 16.677 micros/op; (approximately 60,000 reads per second)
readseq : 0.476 micros/op; 232.3 MB/s
readreverse : 0.724 micros/op; 152.9 MB/s
LevelDB compacts its underlying storage data in the background to
improve read performance. The results listed above were done
immediately after a lot of random writes. The results after
compactions (which are usually triggered automatically) are better.
readrandom : 11.602 micros/op; (approximately 85,000 reads per second)
readseq : 0.423 micros/op; 261.8 MB/s
readreverse : 0.663 micros/op; 166.9 MB/s
Some of the high cost of reads comes from repeated decompression of blocks
read from disk. If we supply enough cache to the leveldb so it can hold the
uncompressed blocks in memory, the read performance improves again:
readrandom : 9.775 micros/op; (approximately 100,000 reads per second before compaction)
readrandom : 5.215 micros/op; (approximately 190,000 reads per second after compaction)
## Repository contents
See [doc/index.md](doc/index.md) for more explanation. See
[doc/impl.md](doc/impl.md) for a brief overview of the implementation.
The public interface is in include/leveldb/*.h. Callers should not include or
rely on the details of any other header files in this package. Those
internal APIs may be changed without warning.
Guide to header files:
* **include/leveldb/db.h**: Main interface to the DB: Start here.
* **include/leveldb/options.h**: Control over the behavior of an entire database,
and also control over the behavior of individual reads and writes.
* **include/leveldb/comparator.h**: Abstraction for user-specified comparison function.
If you want just bytewise comparison of keys, you can use the default
comparator, but clients can write their own comparator implementations if they
want custom ordering (e.g. to handle different character encodings, etc.).
* **include/leveldb/iterator.h**: Interface for iterating over data. You can get
an iterator from a DB object.
* **include/leveldb/write_batch.h**: Interface for atomically applying multiple
updates to a database.
* **include/leveldb/slice.h**: A simple module for maintaining a pointer and a
length into some other byte array.
* **include/leveldb/status.h**: Status is returned from many of the public interfaces
and is used to report success and various kinds of errors.
* **include/leveldb/env.h**:
Abstraction of the OS environment. A posix implementation of this interface is
in util/env_posix.cc.
* **include/leveldb/table.h, include/leveldb/table_builder.h**: Lower-level modules that most
clients probably won't use directly.
+14
View File
@@ -0,0 +1,14 @@
ss
- Stats
db
- Maybe implement DB::BulkDeleteForRange(start_key, end_key)
that would blow away files whose ranges are entirely contained
within [start_key..end_key]? For Chrome, deletion of obsolete
object stores, etc. can be done in the background anyway, so
probably not that important.
- There have been requests for MultiGet.
After a range is completely deleted, what gets rid of the
corresponding files if we do no future changes to that range. Make
the conditions for triggering compactions fire in more situations?
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,92 @@
// Copyright (c) 2019 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <cinttypes>
#include <cstdio>
#include <string>
#include "gtest/gtest.h"
#include "benchmark/benchmark.h"
#include "db/version_set.h"
#include "leveldb/comparator.h"
#include "leveldb/db.h"
#include "leveldb/env.h"
#include "leveldb/options.h"
#include "port/port.h"
#include "util/mutexlock.h"
#include "util/testutil.h"
namespace leveldb {
namespace {
std::string MakeKey(unsigned int num) {
char buf[30];
std::snprintf(buf, sizeof(buf), "%016u", num);
return std::string(buf);
}
void BM_LogAndApply(benchmark::State& state) {
const int num_base_files = state.range(0);
std::string dbname = testing::TempDir() + "leveldb_test_benchmark";
DestroyDB(dbname, Options());
DB* db = nullptr;
Options opts;
opts.create_if_missing = true;
Status s = DB::Open(opts, dbname, &db);
ASSERT_LEVELDB_OK(s);
ASSERT_TRUE(db != nullptr);
delete db;
db = nullptr;
Env* env = Env::Default();
port::Mutex mu;
MutexLock l(&mu);
InternalKeyComparator cmp(BytewiseComparator());
Options options;
VersionSet vset(dbname, &options, nullptr, &cmp);
bool save_manifest;
ASSERT_LEVELDB_OK(vset.Recover(&save_manifest));
VersionEdit vbase;
uint64_t fnum = 1;
for (int i = 0; i < num_base_files; i++) {
InternalKey start(MakeKey(2 * fnum), 1, kTypeValue);
InternalKey limit(MakeKey(2 * fnum + 1), 1, kTypeDeletion);
vbase.AddFile(2, fnum++, 1 /* file size */, start, limit);
}
ASSERT_LEVELDB_OK(vset.LogAndApply(&vbase, &mu));
uint64_t start_micros = env->NowMicros();
for (auto st : state) {
VersionEdit vedit;
vedit.RemoveFile(2, fnum);
InternalKey start(MakeKey(2 * fnum), 1, kTypeValue);
InternalKey limit(MakeKey(2 * fnum + 1), 1, kTypeDeletion);
vedit.AddFile(2, fnum++, 1 /* file size */, start, limit);
vset.LogAndApply(&vedit, &mu);
}
uint64_t stop_micros = env->NowMicros();
unsigned int us = stop_micros - start_micros;
char buf[16];
std::snprintf(buf, sizeof(buf), "%d", num_base_files);
std::fprintf(stderr,
"BM_LogAndApply/%-6s %8" PRIu64
" iters : %9u us (%7.0f us / iter)\n",
buf, state.iterations(), us, ((float)us) / state.iterations());
}
BENCHMARK(BM_LogAndApply)->Arg(1)->Arg(100)->Arg(10000)->Arg(100000);
} // namespace
} // namespace leveldb
BENCHMARK_MAIN();
@@ -0,0 +1,726 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <sqlite3.h>
#include <cstdio>
#include <cstdlib>
#include "util/histogram.h"
#include "util/random.h"
#include "util/testutil.h"
// Comma-separated list of operations to run in the specified order
// Actual benchmarks:
//
// fillseq -- write N values in sequential key order in async mode
// fillseqsync -- write N/100 values in sequential key order in sync mode
// fillseqbatch -- batch write N values in sequential key order in async mode
// fillrandom -- write N values in random key order in async mode
// fillrandsync -- write N/100 values in random key order in sync mode
// fillrandbatch -- batch write N values in sequential key order in async mode
// overwrite -- overwrite N values in random key order in async mode
// fillrand100K -- write N/1000 100K values in random order in async mode
// fillseq100K -- write N/1000 100K values in sequential order in async mode
// readseq -- read N times sequentially
// readrandom -- read N times in random order
// readrand100K -- read N/1000 100K values in sequential order in async mode
static const char* FLAGS_benchmarks =
"fillseq,"
"fillseqsync,"
"fillseqbatch,"
"fillrandom,"
"fillrandsync,"
"fillrandbatch,"
"overwrite,"
"overwritebatch,"
"readrandom,"
"readseq,"
"fillrand100K,"
"fillseq100K,"
"readseq,"
"readrand100K,";
// Number of key/values to place in database
static int FLAGS_num = 1000000;
// Number of read operations to do. If negative, do FLAGS_num reads.
static int FLAGS_reads = -1;
// Size of each value
static int FLAGS_value_size = 100;
// Print histogram of operation timings
static bool FLAGS_histogram = false;
// Arrange to generate values that shrink to this fraction of
// their original size after compression
static double FLAGS_compression_ratio = 0.5;
// Page size. Default 1 KB.
static int FLAGS_page_size = 1024;
// Number of pages.
// Default cache size = FLAGS_page_size * FLAGS_num_pages = 4 MB.
static int FLAGS_num_pages = 4096;
// If true, do not destroy the existing database. If you set this
// flag and also specify a benchmark that wants a fresh database, that
// benchmark will fail.
static bool FLAGS_use_existing_db = false;
// If true, the SQLite table has ROWIDs.
static bool FLAGS_use_rowids = false;
// If true, we allow batch writes to occur
static bool FLAGS_transaction = true;
// If true, we enable Write-Ahead Logging
static bool FLAGS_WAL_enabled = true;
// Use the db with the following name.
static const char* FLAGS_db = nullptr;
inline static void ExecErrorCheck(int status, char* err_msg) {
if (status != SQLITE_OK) {
std::fprintf(stderr, "SQL error: %s\n", err_msg);
sqlite3_free(err_msg);
std::exit(1);
}
}
inline static void StepErrorCheck(int status) {
if (status != SQLITE_DONE) {
std::fprintf(stderr, "SQL step error: status = %d\n", status);
std::exit(1);
}
}
inline static void ErrorCheck(int status) {
if (status != SQLITE_OK) {
std::fprintf(stderr, "sqlite3 error: status = %d\n", status);
std::exit(1);
}
}
inline static void WalCheckpoint(sqlite3* db_) {
// Flush all writes to disk
if (FLAGS_WAL_enabled) {
sqlite3_wal_checkpoint_v2(db_, nullptr, SQLITE_CHECKPOINT_FULL, nullptr,
nullptr);
}
}
namespace leveldb {
// Helper for quickly generating random data.
namespace {
class RandomGenerator {
private:
std::string data_;
int pos_;
public:
RandomGenerator() {
// We use a limited amount of data over and over again and ensure
// that it is larger than the compression window (32KB), and also
// large enough to serve all typical value sizes we want to write.
Random rnd(301);
std::string piece;
while (data_.size() < 1048576) {
// Add a short fragment that is as compressible as specified
// by FLAGS_compression_ratio.
test::CompressibleString(&rnd, FLAGS_compression_ratio, 100, &piece);
data_.append(piece);
}
pos_ = 0;
}
Slice Generate(int len) {
if (pos_ + len > data_.size()) {
pos_ = 0;
assert(len < data_.size());
}
pos_ += len;
return Slice(data_.data() + pos_ - len, len);
}
};
static Slice TrimSpace(Slice s) {
int start = 0;
while (start < s.size() && isspace(s[start])) {
start++;
}
int limit = s.size();
while (limit > start && isspace(s[limit - 1])) {
limit--;
}
return Slice(s.data() + start, limit - start);
}
} // namespace
class Benchmark {
private:
sqlite3* db_;
int db_num_;
int num_;
int reads_;
double start_;
double last_op_finish_;
int64_t bytes_;
std::string message_;
Histogram hist_;
RandomGenerator gen_;
Random rand_;
// State kept for progress messages
int done_;
int next_report_; // When to report next
void PrintHeader() {
const int kKeySize = 16;
PrintEnvironment();
std::fprintf(stdout, "Keys: %d bytes each\n", kKeySize);
std::fprintf(stdout, "Values: %d bytes each\n", FLAGS_value_size);
std::fprintf(stdout, "Entries: %d\n", num_);
std::fprintf(stdout, "RawSize: %.1f MB (estimated)\n",
((static_cast<int64_t>(kKeySize + FLAGS_value_size) * num_) /
1048576.0));
PrintWarnings();
std::fprintf(stdout, "------------------------------------------------\n");
}
void PrintWarnings() {
#if defined(__GNUC__) && !defined(__OPTIMIZE__)
std::fprintf(
stdout,
"WARNING: Optimization is disabled: benchmarks unnecessarily slow\n");
#endif
#ifndef NDEBUG
std::fprintf(
stdout,
"WARNING: Assertions are enabled; benchmarks unnecessarily slow\n");
#endif
}
void PrintEnvironment() {
std::fprintf(stderr, "SQLite: version %s\n", SQLITE_VERSION);
#if defined(__linux)
time_t now = time(nullptr);
std::fprintf(stderr, "Date: %s",
ctime(&now)); // ctime() adds newline
FILE* cpuinfo = std::fopen("/proc/cpuinfo", "r");
if (cpuinfo != nullptr) {
char line[1000];
int num_cpus = 0;
std::string cpu_type;
std::string cache_size;
while (fgets(line, sizeof(line), cpuinfo) != nullptr) {
const char* sep = strchr(line, ':');
if (sep == nullptr) {
continue;
}
Slice key = TrimSpace(Slice(line, sep - 1 - line));
Slice val = TrimSpace(Slice(sep + 1));
if (key == "model name") {
++num_cpus;
cpu_type = val.ToString();
} else if (key == "cache size") {
cache_size = val.ToString();
}
}
std::fclose(cpuinfo);
std::fprintf(stderr, "CPU: %d * %s\n", num_cpus, cpu_type.c_str());
std::fprintf(stderr, "CPUCache: %s\n", cache_size.c_str());
}
#endif
}
void Start() {
start_ = Env::Default()->NowMicros() * 1e-6;
bytes_ = 0;
message_.clear();
last_op_finish_ = start_;
hist_.Clear();
done_ = 0;
next_report_ = 100;
}
void FinishedSingleOp() {
if (FLAGS_histogram) {
double now = Env::Default()->NowMicros() * 1e-6;
double micros = (now - last_op_finish_) * 1e6;
hist_.Add(micros);
if (micros > 20000) {
std::fprintf(stderr, "long op: %.1f micros%30s\r", micros, "");
std::fflush(stderr);
}
last_op_finish_ = now;
}
done_++;
if (done_ >= next_report_) {
if (next_report_ < 1000)
next_report_ += 100;
else if (next_report_ < 5000)
next_report_ += 500;
else if (next_report_ < 10000)
next_report_ += 1000;
else if (next_report_ < 50000)
next_report_ += 5000;
else if (next_report_ < 100000)
next_report_ += 10000;
else if (next_report_ < 500000)
next_report_ += 50000;
else
next_report_ += 100000;
std::fprintf(stderr, "... finished %d ops%30s\r", done_, "");
std::fflush(stderr);
}
}
void Stop(const Slice& name) {
double finish = Env::Default()->NowMicros() * 1e-6;
// Pretend at least one op was done in case we are running a benchmark
// that does not call FinishedSingleOp().
if (done_ < 1) done_ = 1;
if (bytes_ > 0) {
char rate[100];
std::snprintf(rate, sizeof(rate), "%6.1f MB/s",
(bytes_ / 1048576.0) / (finish - start_));
if (!message_.empty()) {
message_ = std::string(rate) + " " + message_;
} else {
message_ = rate;
}
}
std::fprintf(stdout, "%-12s : %11.3f micros/op;%s%s\n",
name.ToString().c_str(), (finish - start_) * 1e6 / done_,
(message_.empty() ? "" : " "), message_.c_str());
if (FLAGS_histogram) {
std::fprintf(stdout, "Microseconds per op:\n%s\n",
hist_.ToString().c_str());
}
std::fflush(stdout);
}
public:
enum Order { SEQUENTIAL, RANDOM };
enum DBState { FRESH, EXISTING };
Benchmark()
: db_(nullptr),
db_num_(0),
num_(FLAGS_num),
reads_(FLAGS_reads < 0 ? FLAGS_num : FLAGS_reads),
bytes_(0),
rand_(301) {
std::vector<std::string> files;
std::string test_dir;
Env::Default()->GetTestDirectory(&test_dir);
Env::Default()->GetChildren(test_dir, &files);
if (!FLAGS_use_existing_db) {
for (int i = 0; i < files.size(); i++) {
if (Slice(files[i]).starts_with("dbbench_sqlite3")) {
std::string file_name(test_dir);
file_name += "/";
file_name += files[i];
Env::Default()->RemoveFile(file_name.c_str());
}
}
}
}
~Benchmark() {
int status = sqlite3_close(db_);
ErrorCheck(status);
}
void Run() {
PrintHeader();
Open();
const char* benchmarks = FLAGS_benchmarks;
while (benchmarks != nullptr) {
const char* sep = strchr(benchmarks, ',');
Slice name;
if (sep == nullptr) {
name = benchmarks;
benchmarks = nullptr;
} else {
name = Slice(benchmarks, sep - benchmarks);
benchmarks = sep + 1;
}
bytes_ = 0;
Start();
bool known = true;
bool write_sync = false;
if (name == Slice("fillseq")) {
Write(write_sync, SEQUENTIAL, FRESH, num_, FLAGS_value_size, 1);
WalCheckpoint(db_);
} else if (name == Slice("fillseqbatch")) {
Write(write_sync, SEQUENTIAL, FRESH, num_, FLAGS_value_size, 1000);
WalCheckpoint(db_);
} else if (name == Slice("fillrandom")) {
Write(write_sync, RANDOM, FRESH, num_, FLAGS_value_size, 1);
WalCheckpoint(db_);
} else if (name == Slice("fillrandbatch")) {
Write(write_sync, RANDOM, FRESH, num_, FLAGS_value_size, 1000);
WalCheckpoint(db_);
} else if (name == Slice("overwrite")) {
Write(write_sync, RANDOM, EXISTING, num_, FLAGS_value_size, 1);
WalCheckpoint(db_);
} else if (name == Slice("overwritebatch")) {
Write(write_sync, RANDOM, EXISTING, num_, FLAGS_value_size, 1000);
WalCheckpoint(db_);
} else if (name == Slice("fillrandsync")) {
write_sync = true;
Write(write_sync, RANDOM, FRESH, num_ / 100, FLAGS_value_size, 1);
WalCheckpoint(db_);
} else if (name == Slice("fillseqsync")) {
write_sync = true;
Write(write_sync, SEQUENTIAL, FRESH, num_ / 100, FLAGS_value_size, 1);
WalCheckpoint(db_);
} else if (name == Slice("fillrand100K")) {
Write(write_sync, RANDOM, FRESH, num_ / 1000, 100 * 1000, 1);
WalCheckpoint(db_);
} else if (name == Slice("fillseq100K")) {
Write(write_sync, SEQUENTIAL, FRESH, num_ / 1000, 100 * 1000, 1);
WalCheckpoint(db_);
} else if (name == Slice("readseq")) {
ReadSequential();
} else if (name == Slice("readrandom")) {
Read(RANDOM, 1);
} else if (name == Slice("readrand100K")) {
int n = reads_;
reads_ /= 1000;
Read(RANDOM, 1);
reads_ = n;
} else {
known = false;
if (name != Slice()) { // No error message for empty name
std::fprintf(stderr, "unknown benchmark '%s'\n",
name.ToString().c_str());
}
}
if (known) {
Stop(name);
}
}
}
void Open() {
assert(db_ == nullptr);
int status;
char file_name[100];
char* err_msg = nullptr;
db_num_++;
// Open database
std::string tmp_dir;
Env::Default()->GetTestDirectory(&tmp_dir);
std::snprintf(file_name, sizeof(file_name), "%s/dbbench_sqlite3-%d.db",
tmp_dir.c_str(), db_num_);
status = sqlite3_open(file_name, &db_);
if (status) {
std::fprintf(stderr, "open error: %s\n", sqlite3_errmsg(db_));
std::exit(1);
}
// Change SQLite cache size
char cache_size[100];
std::snprintf(cache_size, sizeof(cache_size), "PRAGMA cache_size = %d",
FLAGS_num_pages);
status = sqlite3_exec(db_, cache_size, nullptr, nullptr, &err_msg);
ExecErrorCheck(status, err_msg);
// FLAGS_page_size is defaulted to 1024
if (FLAGS_page_size != 1024) {
char page_size[100];
std::snprintf(page_size, sizeof(page_size), "PRAGMA page_size = %d",
FLAGS_page_size);
status = sqlite3_exec(db_, page_size, nullptr, nullptr, &err_msg);
ExecErrorCheck(status, err_msg);
}
// Change journal mode to WAL if WAL enabled flag is on
if (FLAGS_WAL_enabled) {
std::string WAL_stmt = "PRAGMA journal_mode = WAL";
// LevelDB's default cache size is a combined 4 MB
std::string WAL_checkpoint = "PRAGMA wal_autocheckpoint = 4096";
status = sqlite3_exec(db_, WAL_stmt.c_str(), nullptr, nullptr, &err_msg);
ExecErrorCheck(status, err_msg);
status =
sqlite3_exec(db_, WAL_checkpoint.c_str(), nullptr, nullptr, &err_msg);
ExecErrorCheck(status, err_msg);
}
// Change locking mode to exclusive and create tables/index for database
std::string locking_stmt = "PRAGMA locking_mode = EXCLUSIVE";
std::string create_stmt =
"CREATE TABLE test (key blob, value blob, PRIMARY KEY(key))";
if (!FLAGS_use_rowids) create_stmt += " WITHOUT ROWID";
std::string stmt_array[] = {locking_stmt, create_stmt};
int stmt_array_length = sizeof(stmt_array) / sizeof(std::string);
for (int i = 0; i < stmt_array_length; i++) {
status =
sqlite3_exec(db_, stmt_array[i].c_str(), nullptr, nullptr, &err_msg);
ExecErrorCheck(status, err_msg);
}
}
void Write(bool write_sync, Order order, DBState state, int num_entries,
int value_size, int entries_per_batch) {
// Create new database if state == FRESH
if (state == FRESH) {
if (FLAGS_use_existing_db) {
message_ = "skipping (--use_existing_db is true)";
return;
}
sqlite3_close(db_);
db_ = nullptr;
Open();
Start();
}
if (num_entries != num_) {
char msg[100];
std::snprintf(msg, sizeof(msg), "(%d ops)", num_entries);
message_ = msg;
}
char* err_msg = nullptr;
int status;
sqlite3_stmt *replace_stmt, *begin_trans_stmt, *end_trans_stmt;
std::string replace_str = "REPLACE INTO test (key, value) VALUES (?, ?)";
std::string begin_trans_str = "BEGIN TRANSACTION;";
std::string end_trans_str = "END TRANSACTION;";
// Check for synchronous flag in options
std::string sync_stmt =
(write_sync) ? "PRAGMA synchronous = FULL" : "PRAGMA synchronous = OFF";
status = sqlite3_exec(db_, sync_stmt.c_str(), nullptr, nullptr, &err_msg);
ExecErrorCheck(status, err_msg);
// Preparing sqlite3 statements
status = sqlite3_prepare_v2(db_, replace_str.c_str(), -1, &replace_stmt,
nullptr);
ErrorCheck(status);
status = sqlite3_prepare_v2(db_, begin_trans_str.c_str(), -1,
&begin_trans_stmt, nullptr);
ErrorCheck(status);
status = sqlite3_prepare_v2(db_, end_trans_str.c_str(), -1, &end_trans_stmt,
nullptr);
ErrorCheck(status);
bool transaction = (entries_per_batch > 1);
for (int i = 0; i < num_entries; i += entries_per_batch) {
// Begin write transaction
if (FLAGS_transaction && transaction) {
status = sqlite3_step(begin_trans_stmt);
StepErrorCheck(status);
status = sqlite3_reset(begin_trans_stmt);
ErrorCheck(status);
}
// Create and execute SQL statements
for (int j = 0; j < entries_per_batch; j++) {
const char* value = gen_.Generate(value_size).data();
// Create values for key-value pair
const int k =
(order == SEQUENTIAL) ? i + j : (rand_.Next() % num_entries);
char key[100];
std::snprintf(key, sizeof(key), "%016d", k);
// Bind KV values into replace_stmt
status = sqlite3_bind_blob(replace_stmt, 1, key, 16, SQLITE_STATIC);
ErrorCheck(status);
status = sqlite3_bind_blob(replace_stmt, 2, value, value_size,
SQLITE_STATIC);
ErrorCheck(status);
// Execute replace_stmt
bytes_ += value_size + strlen(key);
status = sqlite3_step(replace_stmt);
StepErrorCheck(status);
// Reset SQLite statement for another use
status = sqlite3_clear_bindings(replace_stmt);
ErrorCheck(status);
status = sqlite3_reset(replace_stmt);
ErrorCheck(status);
FinishedSingleOp();
}
// End write transaction
if (FLAGS_transaction && transaction) {
status = sqlite3_step(end_trans_stmt);
StepErrorCheck(status);
status = sqlite3_reset(end_trans_stmt);
ErrorCheck(status);
}
}
status = sqlite3_finalize(replace_stmt);
ErrorCheck(status);
status = sqlite3_finalize(begin_trans_stmt);
ErrorCheck(status);
status = sqlite3_finalize(end_trans_stmt);
ErrorCheck(status);
}
void Read(Order order, int entries_per_batch) {
int status;
sqlite3_stmt *read_stmt, *begin_trans_stmt, *end_trans_stmt;
std::string read_str = "SELECT * FROM test WHERE key = ?";
std::string begin_trans_str = "BEGIN TRANSACTION;";
std::string end_trans_str = "END TRANSACTION;";
// Preparing sqlite3 statements
status = sqlite3_prepare_v2(db_, begin_trans_str.c_str(), -1,
&begin_trans_stmt, nullptr);
ErrorCheck(status);
status = sqlite3_prepare_v2(db_, end_trans_str.c_str(), -1, &end_trans_stmt,
nullptr);
ErrorCheck(status);
status = sqlite3_prepare_v2(db_, read_str.c_str(), -1, &read_stmt, nullptr);
ErrorCheck(status);
bool transaction = (entries_per_batch > 1);
for (int i = 0; i < reads_; i += entries_per_batch) {
// Begin read transaction
if (FLAGS_transaction && transaction) {
status = sqlite3_step(begin_trans_stmt);
StepErrorCheck(status);
status = sqlite3_reset(begin_trans_stmt);
ErrorCheck(status);
}
// Create and execute SQL statements
for (int j = 0; j < entries_per_batch; j++) {
// Create key value
char key[100];
int k = (order == SEQUENTIAL) ? i + j : (rand_.Next() % reads_);
std::snprintf(key, sizeof(key), "%016d", k);
// Bind key value into read_stmt
status = sqlite3_bind_blob(read_stmt, 1, key, 16, SQLITE_STATIC);
ErrorCheck(status);
// Execute read statement
while ((status = sqlite3_step(read_stmt)) == SQLITE_ROW) {
}
StepErrorCheck(status);
// Reset SQLite statement for another use
status = sqlite3_clear_bindings(read_stmt);
ErrorCheck(status);
status = sqlite3_reset(read_stmt);
ErrorCheck(status);
FinishedSingleOp();
}
// End read transaction
if (FLAGS_transaction && transaction) {
status = sqlite3_step(end_trans_stmt);
StepErrorCheck(status);
status = sqlite3_reset(end_trans_stmt);
ErrorCheck(status);
}
}
status = sqlite3_finalize(read_stmt);
ErrorCheck(status);
status = sqlite3_finalize(begin_trans_stmt);
ErrorCheck(status);
status = sqlite3_finalize(end_trans_stmt);
ErrorCheck(status);
}
void ReadSequential() {
int status;
sqlite3_stmt* pStmt;
std::string read_str = "SELECT * FROM test ORDER BY key";
status = sqlite3_prepare_v2(db_, read_str.c_str(), -1, &pStmt, nullptr);
ErrorCheck(status);
for (int i = 0; i < reads_ && SQLITE_ROW == sqlite3_step(pStmt); i++) {
bytes_ += sqlite3_column_bytes(pStmt, 1) + sqlite3_column_bytes(pStmt, 2);
FinishedSingleOp();
}
status = sqlite3_finalize(pStmt);
ErrorCheck(status);
}
};
} // namespace leveldb
int main(int argc, char** argv) {
std::string default_db_path;
for (int i = 1; i < argc; i++) {
double d;
int n;
char junk;
if (leveldb::Slice(argv[i]).starts_with("--benchmarks=")) {
FLAGS_benchmarks = argv[i] + strlen("--benchmarks=");
} else if (sscanf(argv[i], "--histogram=%d%c", &n, &junk) == 1 &&
(n == 0 || n == 1)) {
FLAGS_histogram = n;
} else if (sscanf(argv[i], "--compression_ratio=%lf%c", &d, &junk) == 1) {
FLAGS_compression_ratio = d;
} else if (sscanf(argv[i], "--use_existing_db=%d%c", &n, &junk) == 1 &&
(n == 0 || n == 1)) {
FLAGS_use_existing_db = n;
} else if (sscanf(argv[i], "--use_rowids=%d%c", &n, &junk) == 1 &&
(n == 0 || n == 1)) {
FLAGS_use_rowids = n;
} else if (sscanf(argv[i], "--num=%d%c", &n, &junk) == 1) {
FLAGS_num = n;
} else if (sscanf(argv[i], "--reads=%d%c", &n, &junk) == 1) {
FLAGS_reads = n;
} else if (sscanf(argv[i], "--value_size=%d%c", &n, &junk) == 1) {
FLAGS_value_size = n;
} else if (leveldb::Slice(argv[i]) == leveldb::Slice("--no_transaction")) {
FLAGS_transaction = false;
} else if (sscanf(argv[i], "--page_size=%d%c", &n, &junk) == 1) {
FLAGS_page_size = n;
} else if (sscanf(argv[i], "--num_pages=%d%c", &n, &junk) == 1) {
FLAGS_num_pages = n;
} else if (sscanf(argv[i], "--WAL_enabled=%d%c", &n, &junk) == 1 &&
(n == 0 || n == 1)) {
FLAGS_WAL_enabled = n;
} else if (strncmp(argv[i], "--db=", 5) == 0) {
FLAGS_db = argv[i] + 5;
} else {
std::fprintf(stderr, "Invalid flag '%s'\n", argv[i]);
std::exit(1);
}
}
// Choose a location for the test database if none given with --db=<path>
if (FLAGS_db == nullptr) {
leveldb::Env::Default()->GetTestDirectory(&default_db_path);
default_db_path += "/dbbench";
FLAGS_db = default_db_path.c_str();
}
leveldb::Benchmark benchmark;
benchmark.Run();
return 0;
}
@@ -0,0 +1,531 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <kcpolydb.h>
#include <cstdio>
#include <cstdlib>
#include "util/histogram.h"
#include "util/random.h"
#include "util/testutil.h"
// Comma-separated list of operations to run in the specified order
// Actual benchmarks:
//
// fillseq -- write N values in sequential key order in async mode
// fillrandom -- write N values in random key order in async mode
// overwrite -- overwrite N values in random key order in async mode
// fillseqsync -- write N/100 values in sequential key order in sync mode
// fillrandsync -- write N/100 values in random key order in sync mode
// fillrand100K -- write N/1000 100K values in random order in async mode
// fillseq100K -- write N/1000 100K values in seq order in async mode
// readseq -- read N times sequentially
// readseq100K -- read N/1000 100K values in sequential order in async mode
// readrand100K -- read N/1000 100K values in sequential order in async mode
// readrandom -- read N times in random order
static const char* FLAGS_benchmarks =
"fillseq,"
"fillseqsync,"
"fillrandsync,"
"fillrandom,"
"overwrite,"
"readrandom,"
"readseq,"
"fillrand100K,"
"fillseq100K,"
"readseq100K,"
"readrand100K,";
// Number of key/values to place in database
static int FLAGS_num = 1000000;
// Number of read operations to do. If negative, do FLAGS_num reads.
static int FLAGS_reads = -1;
// Size of each value
static int FLAGS_value_size = 100;
// Arrange to generate values that shrink to this fraction of
// their original size after compression
static double FLAGS_compression_ratio = 0.5;
// Print histogram of operation timings
static bool FLAGS_histogram = false;
// Cache size. Default 4 MB
static int FLAGS_cache_size = 4194304;
// Page size. Default 1 KB
static int FLAGS_page_size = 1024;
// If true, do not destroy the existing database. If you set this
// flag and also specify a benchmark that wants a fresh database, that
// benchmark will fail.
static bool FLAGS_use_existing_db = false;
// Compression flag. If true, compression is on. If false, compression
// is off.
static bool FLAGS_compression = true;
// Use the db with the following name.
static const char* FLAGS_db = nullptr;
inline static void DBSynchronize(kyotocabinet::TreeDB* db_) {
// Synchronize will flush writes to disk
if (!db_->synchronize()) {
std::fprintf(stderr, "synchronize error: %s\n", db_->error().name());
}
}
namespace leveldb {
// Helper for quickly generating random data.
namespace {
class RandomGenerator {
private:
std::string data_;
int pos_;
public:
RandomGenerator() {
// We use a limited amount of data over and over again and ensure
// that it is larger than the compression window (32KB), and also
// large enough to serve all typical value sizes we want to write.
Random rnd(301);
std::string piece;
while (data_.size() < 1048576) {
// Add a short fragment that is as compressible as specified
// by FLAGS_compression_ratio.
test::CompressibleString(&rnd, FLAGS_compression_ratio, 100, &piece);
data_.append(piece);
}
pos_ = 0;
}
Slice Generate(int len) {
if (pos_ + len > data_.size()) {
pos_ = 0;
assert(len < data_.size());
}
pos_ += len;
return Slice(data_.data() + pos_ - len, len);
}
};
static Slice TrimSpace(Slice s) {
int start = 0;
while (start < s.size() && isspace(s[start])) {
start++;
}
int limit = s.size();
while (limit > start && isspace(s[limit - 1])) {
limit--;
}
return Slice(s.data() + start, limit - start);
}
} // namespace
class Benchmark {
private:
kyotocabinet::TreeDB* db_;
int db_num_;
int num_;
int reads_;
double start_;
double last_op_finish_;
int64_t bytes_;
std::string message_;
Histogram hist_;
RandomGenerator gen_;
Random rand_;
kyotocabinet::LZOCompressor<kyotocabinet::LZO::RAW> comp_;
// State kept for progress messages
int done_;
int next_report_; // When to report next
void PrintHeader() {
const int kKeySize = 16;
PrintEnvironment();
std::fprintf(stdout, "Keys: %d bytes each\n", kKeySize);
std::fprintf(
stdout, "Values: %d bytes each (%d bytes after compression)\n",
FLAGS_value_size,
static_cast<int>(FLAGS_value_size * FLAGS_compression_ratio + 0.5));
std::fprintf(stdout, "Entries: %d\n", num_);
std::fprintf(stdout, "RawSize: %.1f MB (estimated)\n",
((static_cast<int64_t>(kKeySize + FLAGS_value_size) * num_) /
1048576.0));
std::fprintf(
stdout, "FileSize: %.1f MB (estimated)\n",
(((kKeySize + FLAGS_value_size * FLAGS_compression_ratio) * num_) /
1048576.0));
PrintWarnings();
std::fprintf(stdout, "------------------------------------------------\n");
}
void PrintWarnings() {
#if defined(__GNUC__) && !defined(__OPTIMIZE__)
std::fprintf(
stdout,
"WARNING: Optimization is disabled: benchmarks unnecessarily slow\n");
#endif
#ifndef NDEBUG
std::fprintf(
stdout,
"WARNING: Assertions are enabled; benchmarks unnecessarily slow\n");
#endif
}
void PrintEnvironment() {
std::fprintf(
stderr, "Kyoto Cabinet: version %s, lib ver %d, lib rev %d\n",
kyotocabinet::VERSION, kyotocabinet::LIBVER, kyotocabinet::LIBREV);
#if defined(__linux)
time_t now = time(nullptr);
std::fprintf(stderr, "Date: %s",
ctime(&now)); // ctime() adds newline
FILE* cpuinfo = std::fopen("/proc/cpuinfo", "r");
if (cpuinfo != nullptr) {
char line[1000];
int num_cpus = 0;
std::string cpu_type;
std::string cache_size;
while (fgets(line, sizeof(line), cpuinfo) != nullptr) {
const char* sep = strchr(line, ':');
if (sep == nullptr) {
continue;
}
Slice key = TrimSpace(Slice(line, sep - 1 - line));
Slice val = TrimSpace(Slice(sep + 1));
if (key == "model name") {
++num_cpus;
cpu_type = val.ToString();
} else if (key == "cache size") {
cache_size = val.ToString();
}
}
std::fclose(cpuinfo);
std::fprintf(stderr, "CPU: %d * %s\n", num_cpus,
cpu_type.c_str());
std::fprintf(stderr, "CPUCache: %s\n", cache_size.c_str());
}
#endif
}
void Start() {
start_ = Env::Default()->NowMicros() * 1e-6;
bytes_ = 0;
message_.clear();
last_op_finish_ = start_;
hist_.Clear();
done_ = 0;
next_report_ = 100;
}
void FinishedSingleOp() {
if (FLAGS_histogram) {
double now = Env::Default()->NowMicros() * 1e-6;
double micros = (now - last_op_finish_) * 1e6;
hist_.Add(micros);
if (micros > 20000) {
std::fprintf(stderr, "long op: %.1f micros%30s\r", micros, "");
std::fflush(stderr);
}
last_op_finish_ = now;
}
done_++;
if (done_ >= next_report_) {
if (next_report_ < 1000)
next_report_ += 100;
else if (next_report_ < 5000)
next_report_ += 500;
else if (next_report_ < 10000)
next_report_ += 1000;
else if (next_report_ < 50000)
next_report_ += 5000;
else if (next_report_ < 100000)
next_report_ += 10000;
else if (next_report_ < 500000)
next_report_ += 50000;
else
next_report_ += 100000;
std::fprintf(stderr, "... finished %d ops%30s\r", done_, "");
std::fflush(stderr);
}
}
void Stop(const Slice& name) {
double finish = Env::Default()->NowMicros() * 1e-6;
// Pretend at least one op was done in case we are running a benchmark
// that does not call FinishedSingleOp().
if (done_ < 1) done_ = 1;
if (bytes_ > 0) {
char rate[100];
std::snprintf(rate, sizeof(rate), "%6.1f MB/s",
(bytes_ / 1048576.0) / (finish - start_));
if (!message_.empty()) {
message_ = std::string(rate) + " " + message_;
} else {
message_ = rate;
}
}
std::fprintf(stdout, "%-12s : %11.3f micros/op;%s%s\n",
name.ToString().c_str(), (finish - start_) * 1e6 / done_,
(message_.empty() ? "" : " "), message_.c_str());
if (FLAGS_histogram) {
std::fprintf(stdout, "Microseconds per op:\n%s\n",
hist_.ToString().c_str());
}
std::fflush(stdout);
}
public:
enum Order { SEQUENTIAL, RANDOM };
enum DBState { FRESH, EXISTING };
Benchmark()
: db_(nullptr),
num_(FLAGS_num),
reads_(FLAGS_reads < 0 ? FLAGS_num : FLAGS_reads),
bytes_(0),
rand_(301) {
std::vector<std::string> files;
std::string test_dir;
Env::Default()->GetTestDirectory(&test_dir);
Env::Default()->GetChildren(test_dir.c_str(), &files);
if (!FLAGS_use_existing_db) {
for (int i = 0; i < files.size(); i++) {
if (Slice(files[i]).starts_with("dbbench_polyDB")) {
std::string file_name(test_dir);
file_name += "/";
file_name += files[i];
Env::Default()->RemoveFile(file_name.c_str());
}
}
}
}
~Benchmark() {
if (!db_->close()) {
std::fprintf(stderr, "close error: %s\n", db_->error().name());
}
}
void Run() {
PrintHeader();
Open(false);
const char* benchmarks = FLAGS_benchmarks;
while (benchmarks != nullptr) {
const char* sep = strchr(benchmarks, ',');
Slice name;
if (sep == nullptr) {
name = benchmarks;
benchmarks = nullptr;
} else {
name = Slice(benchmarks, sep - benchmarks);
benchmarks = sep + 1;
}
Start();
bool known = true;
bool write_sync = false;
if (name == Slice("fillseq")) {
Write(write_sync, SEQUENTIAL, FRESH, num_, FLAGS_value_size, 1);
DBSynchronize(db_);
} else if (name == Slice("fillrandom")) {
Write(write_sync, RANDOM, FRESH, num_, FLAGS_value_size, 1);
DBSynchronize(db_);
} else if (name == Slice("overwrite")) {
Write(write_sync, RANDOM, EXISTING, num_, FLAGS_value_size, 1);
DBSynchronize(db_);
} else if (name == Slice("fillrandsync")) {
write_sync = true;
Write(write_sync, RANDOM, FRESH, num_ / 100, FLAGS_value_size, 1);
DBSynchronize(db_);
} else if (name == Slice("fillseqsync")) {
write_sync = true;
Write(write_sync, SEQUENTIAL, FRESH, num_ / 100, FLAGS_value_size, 1);
DBSynchronize(db_);
} else if (name == Slice("fillrand100K")) {
Write(write_sync, RANDOM, FRESH, num_ / 1000, 100 * 1000, 1);
DBSynchronize(db_);
} else if (name == Slice("fillseq100K")) {
Write(write_sync, SEQUENTIAL, FRESH, num_ / 1000, 100 * 1000, 1);
DBSynchronize(db_);
} else if (name == Slice("readseq")) {
ReadSequential();
} else if (name == Slice("readrandom")) {
ReadRandom();
} else if (name == Slice("readrand100K")) {
int n = reads_;
reads_ /= 1000;
ReadRandom();
reads_ = n;
} else if (name == Slice("readseq100K")) {
int n = reads_;
reads_ /= 1000;
ReadSequential();
reads_ = n;
} else {
known = false;
if (name != Slice()) { // No error message for empty name
std::fprintf(stderr, "unknown benchmark '%s'\n",
name.ToString().c_str());
}
}
if (known) {
Stop(name);
}
}
}
private:
void Open(bool sync) {
assert(db_ == nullptr);
// Initialize db_
db_ = new kyotocabinet::TreeDB();
char file_name[100];
db_num_++;
std::string test_dir;
Env::Default()->GetTestDirectory(&test_dir);
std::snprintf(file_name, sizeof(file_name), "%s/dbbench_polyDB-%d.kct",
test_dir.c_str(), db_num_);
// Create tuning options and open the database
int open_options =
kyotocabinet::PolyDB::OWRITER | kyotocabinet::PolyDB::OCREATE;
int tune_options =
kyotocabinet::TreeDB::TSMALL | kyotocabinet::TreeDB::TLINEAR;
if (FLAGS_compression) {
tune_options |= kyotocabinet::TreeDB::TCOMPRESS;
db_->tune_compressor(&comp_);
}
db_->tune_options(tune_options);
db_->tune_page_cache(FLAGS_cache_size);
db_->tune_page(FLAGS_page_size);
db_->tune_map(256LL << 20);
if (sync) {
open_options |= kyotocabinet::PolyDB::OAUTOSYNC;
}
if (!db_->open(file_name, open_options)) {
std::fprintf(stderr, "open error: %s\n", db_->error().name());
}
}
void Write(bool sync, Order order, DBState state, int num_entries,
int value_size, int entries_per_batch) {
// Create new database if state == FRESH
if (state == FRESH) {
if (FLAGS_use_existing_db) {
message_ = "skipping (--use_existing_db is true)";
return;
}
delete db_;
db_ = nullptr;
Open(sync);
Start(); // Do not count time taken to destroy/open
}
if (num_entries != num_) {
char msg[100];
std::snprintf(msg, sizeof(msg), "(%d ops)", num_entries);
message_ = msg;
}
// Write to database
for (int i = 0; i < num_entries; i++) {
const int k = (order == SEQUENTIAL) ? i : (rand_.Next() % num_entries);
char key[100];
std::snprintf(key, sizeof(key), "%016d", k);
bytes_ += value_size + strlen(key);
std::string cpp_key = key;
if (!db_->set(cpp_key, gen_.Generate(value_size).ToString())) {
std::fprintf(stderr, "set error: %s\n", db_->error().name());
}
FinishedSingleOp();
}
}
void ReadSequential() {
kyotocabinet::DB::Cursor* cur = db_->cursor();
cur->jump();
std::string ckey, cvalue;
while (cur->get(&ckey, &cvalue, true)) {
bytes_ += ckey.size() + cvalue.size();
FinishedSingleOp();
}
delete cur;
}
void ReadRandom() {
std::string value;
for (int i = 0; i < reads_; i++) {
char key[100];
const int k = rand_.Next() % reads_;
std::snprintf(key, sizeof(key), "%016d", k);
db_->get(key, &value);
FinishedSingleOp();
}
}
};
} // namespace leveldb
int main(int argc, char** argv) {
std::string default_db_path;
for (int i = 1; i < argc; i++) {
double d;
int n;
char junk;
if (leveldb::Slice(argv[i]).starts_with("--benchmarks=")) {
FLAGS_benchmarks = argv[i] + strlen("--benchmarks=");
} else if (sscanf(argv[i], "--compression_ratio=%lf%c", &d, &junk) == 1) {
FLAGS_compression_ratio = d;
} else if (sscanf(argv[i], "--histogram=%d%c", &n, &junk) == 1 &&
(n == 0 || n == 1)) {
FLAGS_histogram = n;
} else if (sscanf(argv[i], "--num=%d%c", &n, &junk) == 1) {
FLAGS_num = n;
} else if (sscanf(argv[i], "--reads=%d%c", &n, &junk) == 1) {
FLAGS_reads = n;
} else if (sscanf(argv[i], "--value_size=%d%c", &n, &junk) == 1) {
FLAGS_value_size = n;
} else if (sscanf(argv[i], "--cache_size=%d%c", &n, &junk) == 1) {
FLAGS_cache_size = n;
} else if (sscanf(argv[i], "--page_size=%d%c", &n, &junk) == 1) {
FLAGS_page_size = n;
} else if (sscanf(argv[i], "--compression=%d%c", &n, &junk) == 1 &&
(n == 0 || n == 1)) {
FLAGS_compression = (n == 1) ? true : false;
} else if (strncmp(argv[i], "--db=", 5) == 0) {
FLAGS_db = argv[i] + 5;
} else {
std::fprintf(stderr, "Invalid flag '%s'\n", argv[i]);
std::exit(1);
}
}
// Choose a location for the test database if none given with --db=<path>
if (FLAGS_db == nullptr) {
leveldb::Env::Default()->GetTestDirectory(&default_db_path);
default_db_path += "/dbbench";
FLAGS_db = default_db_path.c_str();
}
leveldb::Benchmark benchmark;
benchmark.Run();
return 0;
}
@@ -0,0 +1,9 @@
# Copyright 2019 The LevelDB Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. See the AUTHORS file for names of contributors.
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/leveldbTargets.cmake")
check_required_components(leveldb)
@@ -0,0 +1,110 @@
// Copyright (c) 2013 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "gtest/gtest.h"
#include "db/db_impl.h"
#include "leveldb/cache.h"
#include "leveldb/db.h"
#include "util/testutil.h"
namespace leveldb {
class AutoCompactTest : public testing::Test {
public:
AutoCompactTest() {
dbname_ = testing::TempDir() + "autocompact_test";
tiny_cache_ = NewLRUCache(100);
options_.block_cache = tiny_cache_;
DestroyDB(dbname_, options_);
options_.create_if_missing = true;
options_.compression = kNoCompression;
EXPECT_LEVELDB_OK(DB::Open(options_, dbname_, &db_));
}
~AutoCompactTest() {
delete db_;
DestroyDB(dbname_, Options());
delete tiny_cache_;
}
std::string Key(int i) {
char buf[100];
std::snprintf(buf, sizeof(buf), "key%06d", i);
return std::string(buf);
}
uint64_t Size(const Slice& start, const Slice& limit) {
Range r(start, limit);
uint64_t size;
db_->GetApproximateSizes(&r, 1, &size);
return size;
}
void DoReads(int n);
private:
std::string dbname_;
Cache* tiny_cache_;
Options options_;
DB* db_;
};
static const int kValueSize = 200 * 1024;
static const int kTotalSize = 100 * 1024 * 1024;
static const int kCount = kTotalSize / kValueSize;
// Read through the first n keys repeatedly and check that they get
// compacted (verified by checking the size of the key space).
void AutoCompactTest::DoReads(int n) {
std::string value(kValueSize, 'x');
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
// Fill database
for (int i = 0; i < kCount; i++) {
ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), Key(i), value));
}
ASSERT_LEVELDB_OK(dbi->TEST_CompactMemTable());
// Delete everything
for (int i = 0; i < kCount; i++) {
ASSERT_LEVELDB_OK(db_->Delete(WriteOptions(), Key(i)));
}
ASSERT_LEVELDB_OK(dbi->TEST_CompactMemTable());
// Get initial measurement of the space we will be reading.
const int64_t initial_size = Size(Key(0), Key(n));
const int64_t initial_other_size = Size(Key(n), Key(kCount));
// Read until size drops significantly.
std::string limit_key = Key(n);
for (int read = 0; true; read++) {
ASSERT_LT(read, 100) << "Taking too long to compact";
Iterator* iter = db_->NewIterator(ReadOptions());
for (iter->SeekToFirst();
iter->Valid() && iter->key().ToString() < limit_key; iter->Next()) {
// Drop data
}
delete iter;
// Wait a little bit to allow any triggered compactions to complete.
Env::Default()->SleepForMicroseconds(1000000);
uint64_t size = Size(Key(0), Key(n));
std::fprintf(stderr, "iter %3d => %7.3f MB [other %7.3f MB]\n", read + 1,
size / 1048576.0, Size(Key(n), Key(kCount)) / 1048576.0);
if (size <= initial_size / 10) {
break;
}
}
// Verify that the size of the key space not touched by the reads
// is pretty much unchanged.
const int64_t final_other_size = Size(Key(n), Key(kCount));
ASSERT_LE(final_other_size, initial_other_size + 1048576);
ASSERT_GE(final_other_size, initial_other_size / 5 - 1048576);
}
TEST_F(AutoCompactTest, ReadAll) { DoReads(kCount); }
TEST_F(AutoCompactTest, ReadHalf) { DoReads(kCount / 2); }
} // namespace leveldb
@@ -0,0 +1,82 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/builder.h"
#include "db/dbformat.h"
#include "db/filename.h"
#include "db/table_cache.h"
#include "db/version_edit.h"
#include "leveldb/db.h"
#include "leveldb/env.h"
#include "leveldb/iterator.h"
namespace leveldb {
Status BuildTable(const std::string& dbname, Env* env, const Options& options,
TableCache* table_cache, Iterator* iter, FileMetaData* meta) {
Status s;
meta->file_size = 0;
iter->SeekToFirst();
std::string fname = TableFileName(dbname, meta->number);
if (iter->Valid()) {
WritableFile* file;
s = env->NewWritableFile(fname, &file);
if (!s.ok()) {
return s;
}
TableBuilder* builder = new TableBuilder(options, file);
meta->smallest.DecodeFrom(iter->key());
Slice key;
for (; iter->Valid(); iter->Next()) {
key = iter->key();
builder->Add(key, iter->value());
}
if (!key.empty()) {
meta->largest.DecodeFrom(key);
}
// Finish and check for builder errors
s = builder->Finish();
if (s.ok()) {
meta->file_size = builder->FileSize();
assert(meta->file_size > 0);
}
delete builder;
// Finish and check for file errors
if (s.ok()) {
s = file->Sync();
}
if (s.ok()) {
s = file->Close();
}
delete file;
file = nullptr;
if (s.ok()) {
// Verify that the table is usable
Iterator* it = table_cache->NewIterator(ReadOptions(), meta->number,
meta->file_size);
s = it->status();
delete it;
}
}
// Check for input iterator errors
if (!iter->status().ok()) {
s = iter->status();
}
if (s.ok() && meta->file_size > 0) {
// Keep it
} else {
env->RemoveFile(fname);
}
return s;
}
} // namespace leveldb
@@ -0,0 +1,30 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_DB_BUILDER_H_
#define STORAGE_LEVELDB_DB_BUILDER_H_
#include "leveldb/status.h"
namespace leveldb {
struct Options;
struct FileMetaData;
class Env;
class Iterator;
class TableCache;
class VersionEdit;
// Build a Table file from the contents of *iter. The generated file
// will be named according to meta->number. On success, the rest of
// *meta will be filled with metadata about the generated table.
// If no data is present in *iter, meta->file_size will be set to
// zero, and no Table file will be produced.
Status BuildTable(const std::string& dbname, Env* env, const Options& options,
TableCache* table_cache, Iterator* iter, FileMetaData* meta);
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_BUILDER_H_
+565
View File
@@ -0,0 +1,565 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "leveldb/c.h"
#include <string.h>
#include <cstdint>
#include <cstdlib>
#include "leveldb/cache.h"
#include "leveldb/comparator.h"
#include "leveldb/db.h"
#include "leveldb/env.h"
#include "leveldb/filter_policy.h"
#include "leveldb/iterator.h"
#include "leveldb/options.h"
#include "leveldb/status.h"
#include "leveldb/write_batch.h"
using leveldb::Cache;
using leveldb::Comparator;
using leveldb::CompressionType;
using leveldb::DB;
using leveldb::Env;
using leveldb::FileLock;
using leveldb::FilterPolicy;
using leveldb::Iterator;
using leveldb::kMajorVersion;
using leveldb::kMinorVersion;
using leveldb::Logger;
using leveldb::NewBloomFilterPolicy;
using leveldb::NewLRUCache;
using leveldb::Options;
using leveldb::RandomAccessFile;
using leveldb::Range;
using leveldb::ReadOptions;
using leveldb::SequentialFile;
using leveldb::Slice;
using leveldb::Snapshot;
using leveldb::Status;
using leveldb::WritableFile;
using leveldb::WriteBatch;
using leveldb::WriteOptions;
extern "C" {
struct leveldb_t {
DB* rep;
};
struct leveldb_iterator_t {
Iterator* rep;
};
struct leveldb_writebatch_t {
WriteBatch rep;
};
struct leveldb_snapshot_t {
const Snapshot* rep;
};
struct leveldb_readoptions_t {
ReadOptions rep;
};
struct leveldb_writeoptions_t {
WriteOptions rep;
};
struct leveldb_options_t {
Options rep;
};
struct leveldb_cache_t {
Cache* rep;
};
struct leveldb_seqfile_t {
SequentialFile* rep;
};
struct leveldb_randomfile_t {
RandomAccessFile* rep;
};
struct leveldb_writablefile_t {
WritableFile* rep;
};
struct leveldb_logger_t {
Logger* rep;
};
struct leveldb_filelock_t {
FileLock* rep;
};
struct leveldb_comparator_t : public Comparator {
~leveldb_comparator_t() override { (*destructor_)(state_); }
int Compare(const Slice& a, const Slice& b) const override {
return (*compare_)(state_, a.data(), a.size(), b.data(), b.size());
}
const char* Name() const override { return (*name_)(state_); }
// No-ops since the C binding does not support key shortening methods.
void FindShortestSeparator(std::string*, const Slice&) const override {}
void FindShortSuccessor(std::string* key) const override {}
void* state_;
void (*destructor_)(void*);
int (*compare_)(void*, const char* a, size_t alen, const char* b,
size_t blen);
const char* (*name_)(void*);
};
struct leveldb_filterpolicy_t : public FilterPolicy {
~leveldb_filterpolicy_t() override { (*destructor_)(state_); }
const char* Name() const override { return (*name_)(state_); }
void CreateFilter(const Slice* keys, int n, std::string* dst) const override {
std::vector<const char*> key_pointers(n);
std::vector<size_t> key_sizes(n);
for (int i = 0; i < n; i++) {
key_pointers[i] = keys[i].data();
key_sizes[i] = keys[i].size();
}
size_t len;
char* filter = (*create_)(state_, &key_pointers[0], &key_sizes[0], n, &len);
dst->append(filter, len);
std::free(filter);
}
bool KeyMayMatch(const Slice& key, const Slice& filter) const override {
return (*key_match_)(state_, key.data(), key.size(), filter.data(),
filter.size());
}
void* state_;
void (*destructor_)(void*);
const char* (*name_)(void*);
char* (*create_)(void*, const char* const* key_array,
const size_t* key_length_array, int num_keys,
size_t* filter_length);
uint8_t (*key_match_)(void*, const char* key, size_t length,
const char* filter, size_t filter_length);
};
struct leveldb_env_t {
Env* rep;
bool is_default;
};
static bool SaveError(char** errptr, const Status& s) {
assert(errptr != nullptr);
if (s.ok()) {
return false;
} else if (*errptr == nullptr) {
*errptr = strdup(s.ToString().c_str());
} else {
// TODO(sanjay): Merge with existing error?
std::free(*errptr);
*errptr = strdup(s.ToString().c_str());
}
return true;
}
static char* CopyString(const std::string& str) {
char* result =
reinterpret_cast<char*>(std::malloc(sizeof(char) * str.size()));
std::memcpy(result, str.data(), sizeof(char) * str.size());
return result;
}
leveldb_t* leveldb_open(const leveldb_options_t* options, const char* name,
char** errptr) {
DB* db;
if (SaveError(errptr, DB::Open(options->rep, std::string(name), &db))) {
return nullptr;
}
leveldb_t* result = new leveldb_t;
result->rep = db;
return result;
}
void leveldb_close(leveldb_t* db) {
delete db->rep;
delete db;
}
void leveldb_put(leveldb_t* db, const leveldb_writeoptions_t* options,
const char* key, size_t keylen, const char* val, size_t vallen,
char** errptr) {
SaveError(errptr,
db->rep->Put(options->rep, Slice(key, keylen), Slice(val, vallen)));
}
void leveldb_delete(leveldb_t* db, const leveldb_writeoptions_t* options,
const char* key, size_t keylen, char** errptr) {
SaveError(errptr, db->rep->Delete(options->rep, Slice(key, keylen)));
}
void leveldb_write(leveldb_t* db, const leveldb_writeoptions_t* options,
leveldb_writebatch_t* batch, char** errptr) {
SaveError(errptr, db->rep->Write(options->rep, &batch->rep));
}
char* leveldb_get(leveldb_t* db, const leveldb_readoptions_t* options,
const char* key, size_t keylen, size_t* vallen,
char** errptr) {
char* result = nullptr;
std::string tmp;
Status s = db->rep->Get(options->rep, Slice(key, keylen), &tmp);
if (s.ok()) {
*vallen = tmp.size();
result = CopyString(tmp);
} else {
*vallen = 0;
if (!s.IsNotFound()) {
SaveError(errptr, s);
}
}
return result;
}
leveldb_iterator_t* leveldb_create_iterator(
leveldb_t* db, const leveldb_readoptions_t* options) {
leveldb_iterator_t* result = new leveldb_iterator_t;
result->rep = db->rep->NewIterator(options->rep);
return result;
}
const leveldb_snapshot_t* leveldb_create_snapshot(leveldb_t* db) {
leveldb_snapshot_t* result = new leveldb_snapshot_t;
result->rep = db->rep->GetSnapshot();
return result;
}
void leveldb_release_snapshot(leveldb_t* db,
const leveldb_snapshot_t* snapshot) {
db->rep->ReleaseSnapshot(snapshot->rep);
delete snapshot;
}
char* leveldb_property_value(leveldb_t* db, const char* propname) {
std::string tmp;
if (db->rep->GetProperty(Slice(propname), &tmp)) {
// We use strdup() since we expect human readable output.
return strdup(tmp.c_str());
} else {
return nullptr;
}
}
void leveldb_approximate_sizes(leveldb_t* db, int num_ranges,
const char* const* range_start_key,
const size_t* range_start_key_len,
const char* const* range_limit_key,
const size_t* range_limit_key_len,
uint64_t* sizes) {
Range* ranges = new Range[num_ranges];
for (int i = 0; i < num_ranges; i++) {
ranges[i].start = Slice(range_start_key[i], range_start_key_len[i]);
ranges[i].limit = Slice(range_limit_key[i], range_limit_key_len[i]);
}
db->rep->GetApproximateSizes(ranges, num_ranges, sizes);
delete[] ranges;
}
void leveldb_compact_range(leveldb_t* db, const char* start_key,
size_t start_key_len, const char* limit_key,
size_t limit_key_len) {
Slice a, b;
db->rep->CompactRange(
// Pass null Slice if corresponding "const char*" is null
(start_key ? (a = Slice(start_key, start_key_len), &a) : nullptr),
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
}
void leveldb_destroy_db(const leveldb_options_t* options, const char* name,
char** errptr) {
SaveError(errptr, DestroyDB(name, options->rep));
}
void leveldb_repair_db(const leveldb_options_t* options, const char* name,
char** errptr) {
SaveError(errptr, RepairDB(name, options->rep));
}
void leveldb_iter_destroy(leveldb_iterator_t* iter) {
delete iter->rep;
delete iter;
}
uint8_t leveldb_iter_valid(const leveldb_iterator_t* iter) {
return iter->rep->Valid();
}
void leveldb_iter_seek_to_first(leveldb_iterator_t* iter) {
iter->rep->SeekToFirst();
}
void leveldb_iter_seek_to_last(leveldb_iterator_t* iter) {
iter->rep->SeekToLast();
}
void leveldb_iter_seek(leveldb_iterator_t* iter, const char* k, size_t klen) {
iter->rep->Seek(Slice(k, klen));
}
void leveldb_iter_next(leveldb_iterator_t* iter) { iter->rep->Next(); }
void leveldb_iter_prev(leveldb_iterator_t* iter) { iter->rep->Prev(); }
const char* leveldb_iter_key(const leveldb_iterator_t* iter, size_t* klen) {
Slice s = iter->rep->key();
*klen = s.size();
return s.data();
}
const char* leveldb_iter_value(const leveldb_iterator_t* iter, size_t* vlen) {
Slice s = iter->rep->value();
*vlen = s.size();
return s.data();
}
void leveldb_iter_get_error(const leveldb_iterator_t* iter, char** errptr) {
SaveError(errptr, iter->rep->status());
}
leveldb_writebatch_t* leveldb_writebatch_create() {
return new leveldb_writebatch_t;
}
void leveldb_writebatch_destroy(leveldb_writebatch_t* b) { delete b; }
void leveldb_writebatch_clear(leveldb_writebatch_t* b) { b->rep.Clear(); }
void leveldb_writebatch_put(leveldb_writebatch_t* b, const char* key,
size_t klen, const char* val, size_t vlen) {
b->rep.Put(Slice(key, klen), Slice(val, vlen));
}
void leveldb_writebatch_delete(leveldb_writebatch_t* b, const char* key,
size_t klen) {
b->rep.Delete(Slice(key, klen));
}
void leveldb_writebatch_iterate(const leveldb_writebatch_t* b, void* state,
void (*put)(void*, const char* k, size_t klen,
const char* v, size_t vlen),
void (*deleted)(void*, const char* k,
size_t klen)) {
class H : public WriteBatch::Handler {
public:
void* state_;
void (*put_)(void*, const char* k, size_t klen, const char* v, size_t vlen);
void (*deleted_)(void*, const char* k, size_t klen);
void Put(const Slice& key, const Slice& value) override {
(*put_)(state_, key.data(), key.size(), value.data(), value.size());
}
void Delete(const Slice& key) override {
(*deleted_)(state_, key.data(), key.size());
}
};
H handler;
handler.state_ = state;
handler.put_ = put;
handler.deleted_ = deleted;
b->rep.Iterate(&handler);
}
void leveldb_writebatch_append(leveldb_writebatch_t* destination,
const leveldb_writebatch_t* source) {
destination->rep.Append(source->rep);
}
leveldb_options_t* leveldb_options_create() { return new leveldb_options_t; }
void leveldb_options_destroy(leveldb_options_t* options) { delete options; }
void leveldb_options_set_comparator(leveldb_options_t* opt,
leveldb_comparator_t* cmp) {
opt->rep.comparator = cmp;
}
void leveldb_options_set_filter_policy(leveldb_options_t* opt,
leveldb_filterpolicy_t* policy) {
opt->rep.filter_policy = policy;
}
void leveldb_options_set_create_if_missing(leveldb_options_t* opt, uint8_t v) {
opt->rep.create_if_missing = v;
}
void leveldb_options_set_error_if_exists(leveldb_options_t* opt, uint8_t v) {
opt->rep.error_if_exists = v;
}
void leveldb_options_set_paranoid_checks(leveldb_options_t* opt, uint8_t v) {
opt->rep.paranoid_checks = v;
}
void leveldb_options_set_env(leveldb_options_t* opt, leveldb_env_t* env) {
opt->rep.env = (env ? env->rep : nullptr);
}
void leveldb_options_set_info_log(leveldb_options_t* opt, leveldb_logger_t* l) {
opt->rep.info_log = (l ? l->rep : nullptr);
}
void leveldb_options_set_write_buffer_size(leveldb_options_t* opt, size_t s) {
opt->rep.write_buffer_size = s;
}
void leveldb_options_set_max_open_files(leveldb_options_t* opt, int n) {
opt->rep.max_open_files = n;
}
void leveldb_options_set_cache(leveldb_options_t* opt, leveldb_cache_t* c) {
opt->rep.block_cache = c->rep;
}
void leveldb_options_set_block_size(leveldb_options_t* opt, size_t s) {
opt->rep.block_size = s;
}
void leveldb_options_set_block_restart_interval(leveldb_options_t* opt, int n) {
opt->rep.block_restart_interval = n;
}
void leveldb_options_set_max_file_size(leveldb_options_t* opt, size_t s) {
opt->rep.max_file_size = s;
}
void leveldb_options_set_compression(leveldb_options_t* opt, int t) {
opt->rep.compression = static_cast<CompressionType>(t);
}
leveldb_comparator_t* leveldb_comparator_create(
void* state, void (*destructor)(void*),
int (*compare)(void*, const char* a, size_t alen, const char* b,
size_t blen),
const char* (*name)(void*)) {
leveldb_comparator_t* result = new leveldb_comparator_t;
result->state_ = state;
result->destructor_ = destructor;
result->compare_ = compare;
result->name_ = name;
return result;
}
void leveldb_comparator_destroy(leveldb_comparator_t* cmp) { delete cmp; }
leveldb_filterpolicy_t* leveldb_filterpolicy_create(
void* state, void (*destructor)(void*),
char* (*create_filter)(void*, const char* const* key_array,
const size_t* key_length_array, int num_keys,
size_t* filter_length),
uint8_t (*key_may_match)(void*, const char* key, size_t length,
const char* filter, size_t filter_length),
const char* (*name)(void*)) {
leveldb_filterpolicy_t* result = new leveldb_filterpolicy_t;
result->state_ = state;
result->destructor_ = destructor;
result->create_ = create_filter;
result->key_match_ = key_may_match;
result->name_ = name;
return result;
}
void leveldb_filterpolicy_destroy(leveldb_filterpolicy_t* filter) {
delete filter;
}
leveldb_filterpolicy_t* leveldb_filterpolicy_create_bloom(int bits_per_key) {
// Make a leveldb_filterpolicy_t, but override all of its methods so
// they delegate to a NewBloomFilterPolicy() instead of user
// supplied C functions.
struct Wrapper : public leveldb_filterpolicy_t {
static void DoNothing(void*) {}
~Wrapper() { delete rep_; }
const char* Name() const { return rep_->Name(); }
void CreateFilter(const Slice* keys, int n, std::string* dst) const {
return rep_->CreateFilter(keys, n, dst);
}
bool KeyMayMatch(const Slice& key, const Slice& filter) const {
return rep_->KeyMayMatch(key, filter);
}
const FilterPolicy* rep_;
};
Wrapper* wrapper = new Wrapper;
wrapper->rep_ = NewBloomFilterPolicy(bits_per_key);
wrapper->state_ = nullptr;
wrapper->destructor_ = &Wrapper::DoNothing;
return wrapper;
}
leveldb_readoptions_t* leveldb_readoptions_create() {
return new leveldb_readoptions_t;
}
void leveldb_readoptions_destroy(leveldb_readoptions_t* opt) { delete opt; }
void leveldb_readoptions_set_verify_checksums(leveldb_readoptions_t* opt,
uint8_t v) {
opt->rep.verify_checksums = v;
}
void leveldb_readoptions_set_fill_cache(leveldb_readoptions_t* opt, uint8_t v) {
opt->rep.fill_cache = v;
}
void leveldb_readoptions_set_snapshot(leveldb_readoptions_t* opt,
const leveldb_snapshot_t* snap) {
opt->rep.snapshot = (snap ? snap->rep : nullptr);
}
leveldb_writeoptions_t* leveldb_writeoptions_create() {
return new leveldb_writeoptions_t;
}
void leveldb_writeoptions_destroy(leveldb_writeoptions_t* opt) { delete opt; }
void leveldb_writeoptions_set_sync(leveldb_writeoptions_t* opt, uint8_t v) {
opt->rep.sync = v;
}
leveldb_cache_t* leveldb_cache_create_lru(size_t capacity) {
leveldb_cache_t* c = new leveldb_cache_t;
c->rep = NewLRUCache(capacity);
return c;
}
void leveldb_cache_destroy(leveldb_cache_t* cache) {
delete cache->rep;
delete cache;
}
leveldb_env_t* leveldb_create_default_env() {
leveldb_env_t* result = new leveldb_env_t;
result->rep = Env::Default();
result->is_default = true;
return result;
}
void leveldb_env_destroy(leveldb_env_t* env) {
if (!env->is_default) delete env->rep;
delete env;
}
char* leveldb_env_get_test_directory(leveldb_env_t* env) {
std::string result;
if (!env->rep->GetTestDirectory(&result).ok()) {
return nullptr;
}
char* buffer = static_cast<char*>(std::malloc(result.size() + 1));
std::memcpy(buffer, result.data(), result.size());
buffer[result.size()] = '\0';
return buffer;
}
void leveldb_free(void* ptr) { std::free(ptr); }
int leveldb_major_version() { return kMajorVersion; }
int leveldb_minor_version() { return kMinorVersion; }
} // end extern "C"
+384
View File
@@ -0,0 +1,384 @@
/* Copyright (c) 2011 The LevelDB Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. See the AUTHORS file for names of contributors. */
#include "leveldb/c.h"
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char* phase = "";
static void StartPhase(const char* name) {
fprintf(stderr, "=== Test %s\n", name);
phase = name;
}
#define CheckNoError(err) \
if ((err) != NULL) { \
fprintf(stderr, "%s:%d: %s: %s\n", __FILE__, __LINE__, phase, (err)); \
abort(); \
}
#define CheckCondition(cond) \
if (!(cond)) { \
fprintf(stderr, "%s:%d: %s: %s\n", __FILE__, __LINE__, phase, #cond); \
abort(); \
}
static void CheckEqual(const char* expected, const char* v, size_t n) {
if (expected == NULL && v == NULL) {
// ok
} else if (expected != NULL && v != NULL && n == strlen(expected) &&
memcmp(expected, v, n) == 0) {
// ok
return;
} else {
fprintf(stderr, "%s: expected '%s', got '%s'\n",
phase,
(expected ? expected : "(null)"),
(v ? v : "(null"));
abort();
}
}
static void Free(char** ptr) {
if (*ptr) {
free(*ptr);
*ptr = NULL;
}
}
static void CheckGet(
leveldb_t* db,
const leveldb_readoptions_t* options,
const char* key,
const char* expected) {
char* err = NULL;
size_t val_len;
char* val;
val = leveldb_get(db, options, key, strlen(key), &val_len, &err);
CheckNoError(err);
CheckEqual(expected, val, val_len);
Free(&val);
}
static void CheckIter(leveldb_iterator_t* iter,
const char* key, const char* val) {
size_t len;
const char* str;
str = leveldb_iter_key(iter, &len);
CheckEqual(key, str, len);
str = leveldb_iter_value(iter, &len);
CheckEqual(val, str, len);
}
// Callback from leveldb_writebatch_iterate()
static void CheckPut(void* ptr,
const char* k, size_t klen,
const char* v, size_t vlen) {
int* state = (int*) ptr;
CheckCondition(*state < 2);
switch (*state) {
case 0:
CheckEqual("bar", k, klen);
CheckEqual("b", v, vlen);
break;
case 1:
CheckEqual("box", k, klen);
CheckEqual("c", v, vlen);
break;
}
(*state)++;
}
// Callback from leveldb_writebatch_iterate()
static void CheckDel(void* ptr, const char* k, size_t klen) {
int* state = (int*) ptr;
CheckCondition(*state == 2);
CheckEqual("bar", k, klen);
(*state)++;
}
static void CmpDestroy(void* arg) { }
static int CmpCompare(void* arg, const char* a, size_t alen,
const char* b, size_t blen) {
int n = (alen < blen) ? alen : blen;
int r = memcmp(a, b, n);
if (r == 0) {
if (alen < blen) r = -1;
else if (alen > blen) r = +1;
}
return r;
}
static const char* CmpName(void* arg) {
return "foo";
}
// Custom filter policy
static uint8_t fake_filter_result = 1;
static void FilterDestroy(void* arg) { }
static const char* FilterName(void* arg) {
return "TestFilter";
}
static char* FilterCreate(
void* arg,
const char* const* key_array, const size_t* key_length_array,
int num_keys,
size_t* filter_length) {
*filter_length = 4;
char* result = malloc(4);
memcpy(result, "fake", 4);
return result;
}
uint8_t FilterKeyMatch(void* arg, const char* key, size_t length,
const char* filter, size_t filter_length) {
CheckCondition(filter_length == 4);
CheckCondition(memcmp(filter, "fake", 4) == 0);
return fake_filter_result;
}
int main(int argc, char** argv) {
leveldb_t* db;
leveldb_comparator_t* cmp;
leveldb_cache_t* cache;
leveldb_env_t* env;
leveldb_options_t* options;
leveldb_readoptions_t* roptions;
leveldb_writeoptions_t* woptions;
char* dbname;
char* err = NULL;
int run = -1;
CheckCondition(leveldb_major_version() >= 1);
CheckCondition(leveldb_minor_version() >= 1);
StartPhase("create_objects");
cmp = leveldb_comparator_create(NULL, CmpDestroy, CmpCompare, CmpName);
env = leveldb_create_default_env();
cache = leveldb_cache_create_lru(100000);
dbname = leveldb_env_get_test_directory(env);
CheckCondition(dbname != NULL);
options = leveldb_options_create();
leveldb_options_set_comparator(options, cmp);
leveldb_options_set_error_if_exists(options, 1);
leveldb_options_set_cache(options, cache);
leveldb_options_set_env(options, env);
leveldb_options_set_info_log(options, NULL);
leveldb_options_set_write_buffer_size(options, 100000);
leveldb_options_set_paranoid_checks(options, 1);
leveldb_options_set_max_open_files(options, 10);
leveldb_options_set_block_size(options, 1024);
leveldb_options_set_block_restart_interval(options, 8);
leveldb_options_set_max_file_size(options, 3 << 20);
leveldb_options_set_compression(options, leveldb_no_compression);
roptions = leveldb_readoptions_create();
leveldb_readoptions_set_verify_checksums(roptions, 1);
leveldb_readoptions_set_fill_cache(roptions, 0);
woptions = leveldb_writeoptions_create();
leveldb_writeoptions_set_sync(woptions, 1);
StartPhase("destroy");
leveldb_destroy_db(options, dbname, &err);
Free(&err);
StartPhase("open_error");
db = leveldb_open(options, dbname, &err);
CheckCondition(err != NULL);
Free(&err);
StartPhase("leveldb_free");
db = leveldb_open(options, dbname, &err);
CheckCondition(err != NULL);
leveldb_free(err);
err = NULL;
StartPhase("open");
leveldb_options_set_create_if_missing(options, 1);
db = leveldb_open(options, dbname, &err);
CheckNoError(err);
CheckGet(db, roptions, "foo", NULL);
StartPhase("put");
leveldb_put(db, woptions, "foo", 3, "hello", 5, &err);
CheckNoError(err);
CheckGet(db, roptions, "foo", "hello");
StartPhase("compactall");
leveldb_compact_range(db, NULL, 0, NULL, 0);
CheckGet(db, roptions, "foo", "hello");
StartPhase("compactrange");
leveldb_compact_range(db, "a", 1, "z", 1);
CheckGet(db, roptions, "foo", "hello");
StartPhase("writebatch");
{
leveldb_writebatch_t* wb = leveldb_writebatch_create();
leveldb_writebatch_put(wb, "foo", 3, "a", 1);
leveldb_writebatch_clear(wb);
leveldb_writebatch_put(wb, "bar", 3, "b", 1);
leveldb_writebatch_put(wb, "box", 3, "c", 1);
leveldb_writebatch_t* wb2 = leveldb_writebatch_create();
leveldb_writebatch_delete(wb2, "bar", 3);
leveldb_writebatch_append(wb, wb2);
leveldb_writebatch_destroy(wb2);
leveldb_write(db, woptions, wb, &err);
CheckNoError(err);
CheckGet(db, roptions, "foo", "hello");
CheckGet(db, roptions, "bar", NULL);
CheckGet(db, roptions, "box", "c");
int pos = 0;
leveldb_writebatch_iterate(wb, &pos, CheckPut, CheckDel);
CheckCondition(pos == 3);
leveldb_writebatch_destroy(wb);
}
StartPhase("iter");
{
leveldb_iterator_t* iter = leveldb_create_iterator(db, roptions);
CheckCondition(!leveldb_iter_valid(iter));
leveldb_iter_seek_to_first(iter);
CheckCondition(leveldb_iter_valid(iter));
CheckIter(iter, "box", "c");
leveldb_iter_next(iter);
CheckIter(iter, "foo", "hello");
leveldb_iter_prev(iter);
CheckIter(iter, "box", "c");
leveldb_iter_prev(iter);
CheckCondition(!leveldb_iter_valid(iter));
leveldb_iter_seek_to_last(iter);
CheckIter(iter, "foo", "hello");
leveldb_iter_seek(iter, "b", 1);
CheckIter(iter, "box", "c");
leveldb_iter_get_error(iter, &err);
CheckNoError(err);
leveldb_iter_destroy(iter);
}
StartPhase("approximate_sizes");
{
int i;
int n = 20000;
char keybuf[100];
char valbuf[100];
uint64_t sizes[2];
const char* start[2] = { "a", "k00000000000000010000" };
size_t start_len[2] = { 1, 21 };
const char* limit[2] = { "k00000000000000010000", "z" };
size_t limit_len[2] = { 21, 1 };
leveldb_writeoptions_set_sync(woptions, 0);
for (i = 0; i < n; i++) {
snprintf(keybuf, sizeof(keybuf), "k%020d", i);
snprintf(valbuf, sizeof(valbuf), "v%020d", i);
leveldb_put(db, woptions, keybuf, strlen(keybuf), valbuf, strlen(valbuf),
&err);
CheckNoError(err);
}
leveldb_approximate_sizes(db, 2, start, start_len, limit, limit_len, sizes);
CheckCondition(sizes[0] > 0);
CheckCondition(sizes[1] > 0);
}
StartPhase("property");
{
char* prop = leveldb_property_value(db, "nosuchprop");
CheckCondition(prop == NULL);
prop = leveldb_property_value(db, "leveldb.stats");
CheckCondition(prop != NULL);
Free(&prop);
}
StartPhase("snapshot");
{
const leveldb_snapshot_t* snap;
snap = leveldb_create_snapshot(db);
leveldb_delete(db, woptions, "foo", 3, &err);
CheckNoError(err);
leveldb_readoptions_set_snapshot(roptions, snap);
CheckGet(db, roptions, "foo", "hello");
leveldb_readoptions_set_snapshot(roptions, NULL);
CheckGet(db, roptions, "foo", NULL);
leveldb_release_snapshot(db, snap);
}
StartPhase("repair");
{
leveldb_close(db);
leveldb_options_set_create_if_missing(options, 0);
leveldb_options_set_error_if_exists(options, 0);
leveldb_repair_db(options, dbname, &err);
CheckNoError(err);
db = leveldb_open(options, dbname, &err);
CheckNoError(err);
CheckGet(db, roptions, "foo", NULL);
CheckGet(db, roptions, "bar", NULL);
CheckGet(db, roptions, "box", "c");
leveldb_options_set_create_if_missing(options, 1);
leveldb_options_set_error_if_exists(options, 1);
}
StartPhase("filter");
for (run = 0; run < 2; run++) {
// First run uses custom filter, second run uses bloom filter
CheckNoError(err);
leveldb_filterpolicy_t* policy;
if (run == 0) {
policy = leveldb_filterpolicy_create(
NULL, FilterDestroy, FilterCreate, FilterKeyMatch, FilterName);
} else {
policy = leveldb_filterpolicy_create_bloom(10);
}
// Create new database
leveldb_close(db);
leveldb_destroy_db(options, dbname, &err);
leveldb_options_set_filter_policy(options, policy);
db = leveldb_open(options, dbname, &err);
CheckNoError(err);
leveldb_put(db, woptions, "foo", 3, "foovalue", 8, &err);
CheckNoError(err);
leveldb_put(db, woptions, "bar", 3, "barvalue", 8, &err);
CheckNoError(err);
leveldb_compact_range(db, NULL, 0, NULL, 0);
fake_filter_result = 1;
CheckGet(db, roptions, "foo", "foovalue");
CheckGet(db, roptions, "bar", "barvalue");
if (phase == 0) {
// Must not find value when custom filter returns false
fake_filter_result = 0;
CheckGet(db, roptions, "foo", NULL);
CheckGet(db, roptions, "bar", NULL);
fake_filter_result = 1;
CheckGet(db, roptions, "foo", "foovalue");
CheckGet(db, roptions, "bar", "barvalue");
}
leveldb_options_set_filter_policy(options, NULL);
leveldb_filterpolicy_destroy(policy);
}
StartPhase("cleanup");
leveldb_close(db);
leveldb_options_destroy(options);
leveldb_readoptions_destroy(roptions);
leveldb_writeoptions_destroy(woptions);
leveldb_free(dbname);
leveldb_cache_destroy(cache);
leveldb_comparator_destroy(cmp);
leveldb_env_destroy(env);
fprintf(stderr, "PASS\n");
return 0;
}
@@ -0,0 +1,362 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <sys/types.h>
#include "gtest/gtest.h"
#include "db/db_impl.h"
#include "db/filename.h"
#include "db/log_format.h"
#include "db/version_set.h"
#include "leveldb/cache.h"
#include "leveldb/db.h"
#include "leveldb/table.h"
#include "leveldb/write_batch.h"
#include "util/logging.h"
#include "util/testutil.h"
namespace leveldb {
static const int kValueSize = 1000;
class CorruptionTest : public testing::Test {
public:
CorruptionTest()
: db_(nullptr),
dbname_("/memenv/corruption_test"),
tiny_cache_(NewLRUCache(100)) {
options_.env = &env_;
options_.block_cache = tiny_cache_;
DestroyDB(dbname_, options_);
options_.create_if_missing = true;
Reopen();
options_.create_if_missing = false;
}
~CorruptionTest() {
delete db_;
delete tiny_cache_;
}
Status TryReopen() {
delete db_;
db_ = nullptr;
return DB::Open(options_, dbname_, &db_);
}
void Reopen() { ASSERT_LEVELDB_OK(TryReopen()); }
void RepairDB() {
delete db_;
db_ = nullptr;
ASSERT_LEVELDB_OK(::leveldb::RepairDB(dbname_, options_));
}
void Build(int n) {
std::string key_space, value_space;
WriteBatch batch;
for (int i = 0; i < n; i++) {
// if ((i % 100) == 0) std::fprintf(stderr, "@ %d of %d\n", i, n);
Slice key = Key(i, &key_space);
batch.Clear();
batch.Put(key, Value(i, &value_space));
WriteOptions options;
// Corrupt() doesn't work without this sync on windows; stat reports 0 for
// the file size.
if (i == n - 1) {
options.sync = true;
}
ASSERT_LEVELDB_OK(db_->Write(options, &batch));
}
}
void Check(int min_expected, int max_expected) {
int next_expected = 0;
int missed = 0;
int bad_keys = 0;
int bad_values = 0;
int correct = 0;
std::string value_space;
Iterator* iter = db_->NewIterator(ReadOptions());
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
uint64_t key;
Slice in(iter->key());
if (in == "" || in == "~") {
// Ignore boundary keys.
continue;
}
if (!ConsumeDecimalNumber(&in, &key) || !in.empty() ||
key < next_expected) {
bad_keys++;
continue;
}
missed += (key - next_expected);
next_expected = key + 1;
if (iter->value() != Value(key, &value_space)) {
bad_values++;
} else {
correct++;
}
}
delete iter;
std::fprintf(
stderr,
"expected=%d..%d; got=%d; bad_keys=%d; bad_values=%d; missed=%d\n",
min_expected, max_expected, correct, bad_keys, bad_values, missed);
ASSERT_LE(min_expected, correct);
ASSERT_GE(max_expected, correct);
}
void Corrupt(FileType filetype, int offset, int bytes_to_corrupt) {
// Pick file to corrupt
std::vector<std::string> filenames;
ASSERT_LEVELDB_OK(env_.target()->GetChildren(dbname_, &filenames));
uint64_t number;
FileType type;
std::string fname;
int picked_number = -1;
for (size_t i = 0; i < filenames.size(); i++) {
if (ParseFileName(filenames[i], &number, &type) && type == filetype &&
int(number) > picked_number) { // Pick latest file
fname = dbname_ + "/" + filenames[i];
picked_number = number;
}
}
ASSERT_TRUE(!fname.empty()) << filetype;
uint64_t file_size;
ASSERT_LEVELDB_OK(env_.target()->GetFileSize(fname, &file_size));
if (offset < 0) {
// Relative to end of file; make it absolute
if (-offset > file_size) {
offset = 0;
} else {
offset = file_size + offset;
}
}
if (offset > file_size) {
offset = file_size;
}
if (offset + bytes_to_corrupt > file_size) {
bytes_to_corrupt = file_size - offset;
}
// Do it
std::string contents;
Status s = ReadFileToString(env_.target(), fname, &contents);
ASSERT_TRUE(s.ok()) << s.ToString();
for (int i = 0; i < bytes_to_corrupt; i++) {
contents[i + offset] ^= 0x80;
}
s = WriteStringToFile(env_.target(), contents, fname);
ASSERT_TRUE(s.ok()) << s.ToString();
}
int Property(const std::string& name) {
std::string property;
int result;
if (db_->GetProperty(name, &property) &&
sscanf(property.c_str(), "%d", &result) == 1) {
return result;
} else {
return -1;
}
}
// Return the ith key
Slice Key(int i, std::string* storage) {
char buf[100];
std::snprintf(buf, sizeof(buf), "%016d", i);
storage->assign(buf, strlen(buf));
return Slice(*storage);
}
// Return the value to associate with the specified key
Slice Value(int k, std::string* storage) {
Random r(k);
return test::RandomString(&r, kValueSize, storage);
}
test::ErrorEnv env_;
Options options_;
DB* db_;
private:
std::string dbname_;
Cache* tiny_cache_;
};
TEST_F(CorruptionTest, Recovery) {
Build(100);
Check(100, 100);
Corrupt(kLogFile, 19, 1); // WriteBatch tag for first record
Corrupt(kLogFile, log::kBlockSize + 1000, 1); // Somewhere in second block
Reopen();
// The 64 records in the first two log blocks are completely lost.
Check(36, 36);
}
TEST_F(CorruptionTest, RecoverWriteError) {
env_.writable_file_error_ = true;
Status s = TryReopen();
ASSERT_TRUE(!s.ok());
}
TEST_F(CorruptionTest, NewFileErrorDuringWrite) {
// Do enough writing to force minor compaction
env_.writable_file_error_ = true;
const int num = 3 + (Options().write_buffer_size / kValueSize);
std::string value_storage;
Status s;
for (int i = 0; s.ok() && i < num; i++) {
WriteBatch batch;
batch.Put("a", Value(100, &value_storage));
s = db_->Write(WriteOptions(), &batch);
}
ASSERT_TRUE(!s.ok());
ASSERT_GE(env_.num_writable_file_errors_, 1);
env_.writable_file_error_ = false;
Reopen();
}
TEST_F(CorruptionTest, TableFile) {
Build(100);
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
dbi->TEST_CompactMemTable();
dbi->TEST_CompactRange(0, nullptr, nullptr);
dbi->TEST_CompactRange(1, nullptr, nullptr);
Corrupt(kTableFile, 100, 1);
Check(90, 99);
}
TEST_F(CorruptionTest, TableFileRepair) {
options_.block_size = 2 * kValueSize; // Limit scope of corruption
options_.paranoid_checks = true;
Reopen();
Build(100);
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
dbi->TEST_CompactMemTable();
dbi->TEST_CompactRange(0, nullptr, nullptr);
dbi->TEST_CompactRange(1, nullptr, nullptr);
Corrupt(kTableFile, 100, 1);
RepairDB();
Reopen();
Check(95, 99);
}
TEST_F(CorruptionTest, TableFileIndexData) {
Build(10000); // Enough to build multiple Tables
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
dbi->TEST_CompactMemTable();
Corrupt(kTableFile, -2000, 500);
Reopen();
Check(5000, 9999);
}
TEST_F(CorruptionTest, MissingDescriptor) {
Build(1000);
RepairDB();
Reopen();
Check(1000, 1000);
}
TEST_F(CorruptionTest, SequenceNumberRecovery) {
ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), "foo", "v1"));
ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), "foo", "v2"));
ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), "foo", "v3"));
ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), "foo", "v4"));
ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), "foo", "v5"));
RepairDB();
Reopen();
std::string v;
ASSERT_LEVELDB_OK(db_->Get(ReadOptions(), "foo", &v));
ASSERT_EQ("v5", v);
// Write something. If sequence number was not recovered properly,
// it will be hidden by an earlier write.
ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), "foo", "v6"));
ASSERT_LEVELDB_OK(db_->Get(ReadOptions(), "foo", &v));
ASSERT_EQ("v6", v);
Reopen();
ASSERT_LEVELDB_OK(db_->Get(ReadOptions(), "foo", &v));
ASSERT_EQ("v6", v);
}
TEST_F(CorruptionTest, CorruptedDescriptor) {
ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), "foo", "hello"));
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
dbi->TEST_CompactMemTable();
dbi->TEST_CompactRange(0, nullptr, nullptr);
Corrupt(kDescriptorFile, 0, 1000);
Status s = TryReopen();
ASSERT_TRUE(!s.ok());
RepairDB();
Reopen();
std::string v;
ASSERT_LEVELDB_OK(db_->Get(ReadOptions(), "foo", &v));
ASSERT_EQ("hello", v);
}
TEST_F(CorruptionTest, CompactionInputError) {
Build(10);
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
dbi->TEST_CompactMemTable();
const int last = config::kMaxMemCompactLevel;
ASSERT_EQ(1, Property("leveldb.num-files-at-level" + NumberToString(last)));
Corrupt(kTableFile, 100, 1);
Check(5, 9);
// Force compactions by writing lots of values
Build(10000);
Check(10000, 10000);
}
TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
options_.paranoid_checks = true;
options_.write_buffer_size = 512 << 10;
Reopen();
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
// Make multiple inputs so we need to compact.
for (int i = 0; i < 2; i++) {
Build(10);
dbi->TEST_CompactMemTable();
Corrupt(kTableFile, 100, 1);
env_.SleepForMicroseconds(100000);
}
dbi->CompactRange(nullptr, nullptr);
// Write must fail because of corrupted table
std::string tmp1, tmp2;
Status s = db_->Put(WriteOptions(), Key(5, &tmp1), Value(5, &tmp2));
ASSERT_TRUE(!s.ok()) << "write did not fail in corrupted paranoid db";
}
TEST_F(CorruptionTest, UnrelatedKeys) {
Build(10);
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
dbi->TEST_CompactMemTable();
Corrupt(kTableFile, 100, 1);
std::string tmp1, tmp2;
ASSERT_LEVELDB_OK(
db_->Put(WriteOptions(), Key(1000, &tmp1), Value(1000, &tmp2)));
std::string v;
ASSERT_LEVELDB_OK(db_->Get(ReadOptions(), Key(1000, &tmp1), &v));
ASSERT_EQ(Value(1000, &tmp2).ToString(), v);
dbi->TEST_CompactMemTable();
ASSERT_LEVELDB_OK(db_->Get(ReadOptions(), Key(1000, &tmp1), &v));
ASSERT_EQ(Value(1000, &tmp2).ToString(), v);
}
} // namespace leveldb
File diff suppressed because it is too large Load Diff
+217
View File
@@ -0,0 +1,217 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_DB_DB_IMPL_H_
#define STORAGE_LEVELDB_DB_DB_IMPL_H_
#include <atomic>
#include <deque>
#include <set>
#include <string>
#include "db/dbformat.h"
#include "db/log_writer.h"
#include "db/snapshot.h"
#include "leveldb/db.h"
#include "leveldb/env.h"
#include "port/port.h"
#include "port/thread_annotations.h"
namespace leveldb {
class MemTable;
class TableCache;
class Version;
class VersionEdit;
class VersionSet;
class DBImpl : public DB {
public:
DBImpl(const Options& options, const std::string& dbname);
DBImpl(const DBImpl&) = delete;
DBImpl& operator=(const DBImpl&) = delete;
~DBImpl() override;
// Implementations of the DB interface
Status Put(const WriteOptions&, const Slice& key,
const Slice& value) override;
Status Delete(const WriteOptions&, const Slice& key) override;
Status Write(const WriteOptions& options, WriteBatch* updates) override;
Status Get(const ReadOptions& options, const Slice& key,
std::string* value) override;
Iterator* NewIterator(const ReadOptions&) override;
const Snapshot* GetSnapshot() override;
void ReleaseSnapshot(const Snapshot* snapshot) override;
bool GetProperty(const Slice& property, std::string* value) override;
void GetApproximateSizes(const Range* range, int n, uint64_t* sizes) override;
void CompactRange(const Slice* begin, const Slice* end) override;
// Extra methods (for testing) that are not in the public DB interface
// Compact any files in the named level that overlap [*begin,*end]
void TEST_CompactRange(int level, const Slice* begin, const Slice* end);
// Force current memtable contents to be compacted.
Status TEST_CompactMemTable();
// Return an internal iterator over the current state of the database.
// The keys of this iterator are internal keys (see format.h).
// The returned iterator should be deleted when no longer needed.
Iterator* TEST_NewInternalIterator();
// Return the maximum overlapping data (in bytes) at next level for any
// file at a level >= 1.
int64_t TEST_MaxNextLevelOverlappingBytes();
// Record a sample of bytes read at the specified internal key.
// Samples are taken approximately once every config::kReadBytesPeriod
// bytes.
void RecordReadSample(Slice key);
private:
friend class DB;
struct CompactionState;
struct Writer;
// Information for a manual compaction
struct ManualCompaction {
int level;
bool done;
const InternalKey* begin; // null means beginning of key range
const InternalKey* end; // null means end of key range
InternalKey tmp_storage; // Used to keep track of compaction progress
};
// Per level compaction stats. stats_[level] stores the stats for
// compactions that produced data for the specified "level".
struct CompactionStats {
CompactionStats() : micros(0), bytes_read(0), bytes_written(0) {}
void Add(const CompactionStats& c) {
this->micros += c.micros;
this->bytes_read += c.bytes_read;
this->bytes_written += c.bytes_written;
}
int64_t micros;
int64_t bytes_read;
int64_t bytes_written;
};
Iterator* NewInternalIterator(const ReadOptions&,
SequenceNumber* latest_snapshot,
uint32_t* seed);
Status NewDB();
// Recover the descriptor from persistent storage. May do a significant
// amount of work to recover recently logged updates. Any changes to
// be made to the descriptor are added to *edit.
Status Recover(VersionEdit* edit, bool* save_manifest)
EXCLUSIVE_LOCKS_REQUIRED(mutex_);
void MaybeIgnoreError(Status* s) const;
// Delete any unneeded files and stale in-memory entries.
void RemoveObsoleteFiles() EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Compact the in-memory write buffer to disk. Switches to a new
// log-file/memtable and writes a new descriptor iff successful.
// Errors are recorded in bg_error_.
void CompactMemTable() EXCLUSIVE_LOCKS_REQUIRED(mutex_);
Status RecoverLogFile(uint64_t log_number, bool last_log, bool* save_manifest,
VersionEdit* edit, SequenceNumber* max_sequence)
EXCLUSIVE_LOCKS_REQUIRED(mutex_);
Status WriteLevel0Table(MemTable* mem, VersionEdit* edit, Version* base)
EXCLUSIVE_LOCKS_REQUIRED(mutex_);
Status MakeRoomForWrite(bool force /* compact even if there is room? */)
EXCLUSIVE_LOCKS_REQUIRED(mutex_);
WriteBatch* BuildBatchGroup(Writer** last_writer)
EXCLUSIVE_LOCKS_REQUIRED(mutex_);
void RecordBackgroundError(const Status& s);
void MaybeScheduleCompaction() EXCLUSIVE_LOCKS_REQUIRED(mutex_);
static void BGWork(void* db);
void BackgroundCall();
void BackgroundCompaction() EXCLUSIVE_LOCKS_REQUIRED(mutex_);
void CleanupCompaction(CompactionState* compact)
EXCLUSIVE_LOCKS_REQUIRED(mutex_);
Status DoCompactionWork(CompactionState* compact)
EXCLUSIVE_LOCKS_REQUIRED(mutex_);
Status OpenCompactionOutputFile(CompactionState* compact);
Status FinishCompactionOutputFile(CompactionState* compact, Iterator* input);
Status InstallCompactionResults(CompactionState* compact)
EXCLUSIVE_LOCKS_REQUIRED(mutex_);
const Comparator* user_comparator() const {
return internal_comparator_.user_comparator();
}
// Constant after construction
Env* const env_;
const InternalKeyComparator internal_comparator_;
const InternalFilterPolicy internal_filter_policy_;
const Options options_; // options_.comparator == &internal_comparator_
const bool owns_info_log_;
const bool owns_cache_;
const std::string dbname_;
// table_cache_ provides its own synchronization
TableCache* const table_cache_;
// Lock over the persistent DB state. Non-null iff successfully acquired.
FileLock* db_lock_;
// State below is protected by mutex_
port::Mutex mutex_;
std::atomic<bool> shutting_down_;
port::CondVar background_work_finished_signal_ GUARDED_BY(mutex_);
MemTable* mem_;
MemTable* imm_ GUARDED_BY(mutex_); // Memtable being compacted
std::atomic<bool> has_imm_; // So bg thread can detect non-null imm_
WritableFile* logfile_;
uint64_t logfile_number_ GUARDED_BY(mutex_);
log::Writer* log_;
uint32_t seed_ GUARDED_BY(mutex_); // For sampling.
// Queue of writers.
std::deque<Writer*> writers_ GUARDED_BY(mutex_);
WriteBatch* tmp_batch_ GUARDED_BY(mutex_);
SnapshotList snapshots_ GUARDED_BY(mutex_);
// Set of table files to protect from deletion because they are
// part of ongoing compactions.
std::set<uint64_t> pending_outputs_ GUARDED_BY(mutex_);
// Has a background compaction been scheduled or is running?
bool background_compaction_scheduled_ GUARDED_BY(mutex_);
ManualCompaction* manual_compaction_ GUARDED_BY(mutex_);
VersionSet* const versions_ GUARDED_BY(mutex_);
// Have we encountered a background error in paranoid mode?
Status bg_error_ GUARDED_BY(mutex_);
CompactionStats stats_[config::kNumLevels] GUARDED_BY(mutex_);
};
// Sanitize db options. The caller should delete result.info_log if
// it is not equal to src.info_log.
Options SanitizeOptions(const std::string& db,
const InternalKeyComparator* icmp,
const InternalFilterPolicy* ipolicy,
const Options& src);
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_DB_IMPL_H_
+318
View File
@@ -0,0 +1,318 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/db_iter.h"
#include "db/db_impl.h"
#include "db/dbformat.h"
#include "db/filename.h"
#include "leveldb/env.h"
#include "leveldb/iterator.h"
#include "port/port.h"
#include "util/logging.h"
#include "util/mutexlock.h"
#include "util/random.h"
namespace leveldb {
#if 0
static void DumpInternalIter(Iterator* iter) {
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
ParsedInternalKey k;
if (!ParseInternalKey(iter->key(), &k)) {
std::fprintf(stderr, "Corrupt '%s'\n", EscapeString(iter->key()).c_str());
} else {
std::fprintf(stderr, "@ '%s'\n", k.DebugString().c_str());
}
}
}
#endif
namespace {
// Memtables and sstables that make the DB representation contain
// (userkey,seq,type) => uservalue entries. DBIter
// combines multiple entries for the same userkey found in the DB
// representation into a single entry while accounting for sequence
// numbers, deletion markers, overwrites, etc.
class DBIter : public Iterator {
public:
// Which direction is the iterator currently moving?
// (1) When moving forward, the internal iterator is positioned at
// the exact entry that yields this->key(), this->value()
// (2) When moving backwards, the internal iterator is positioned
// just before all entries whose user key == this->key().
enum Direction { kForward, kReverse };
DBIter(DBImpl* db, const Comparator* cmp, Iterator* iter, SequenceNumber s,
uint32_t seed)
: db_(db),
user_comparator_(cmp),
iter_(iter),
sequence_(s),
direction_(kForward),
valid_(false),
rnd_(seed),
bytes_until_read_sampling_(RandomCompactionPeriod()) {}
DBIter(const DBIter&) = delete;
DBIter& operator=(const DBIter&) = delete;
~DBIter() override { delete iter_; }
bool Valid() const override { return valid_; }
Slice key() const override {
assert(valid_);
return (direction_ == kForward) ? ExtractUserKey(iter_->key()) : saved_key_;
}
Slice value() const override {
assert(valid_);
return (direction_ == kForward) ? iter_->value() : saved_value_;
}
Status status() const override {
if (status_.ok()) {
return iter_->status();
} else {
return status_;
}
}
void Next() override;
void Prev() override;
void Seek(const Slice& target) override;
void SeekToFirst() override;
void SeekToLast() override;
private:
void FindNextUserEntry(bool skipping, std::string* skip);
void FindPrevUserEntry();
bool ParseKey(ParsedInternalKey* key);
inline void SaveKey(const Slice& k, std::string* dst) {
dst->assign(k.data(), k.size());
}
inline void ClearSavedValue() {
if (saved_value_.capacity() > 1048576) {
std::string empty;
swap(empty, saved_value_);
} else {
saved_value_.clear();
}
}
// Picks the number of bytes that can be read until a compaction is scheduled.
size_t RandomCompactionPeriod() {
return rnd_.Uniform(2 * config::kReadBytesPeriod);
}
DBImpl* db_;
const Comparator* const user_comparator_;
Iterator* const iter_;
SequenceNumber const sequence_;
Status status_;
std::string saved_key_; // == current key when direction_==kReverse
std::string saved_value_; // == current raw value when direction_==kReverse
Direction direction_;
bool valid_;
Random rnd_;
size_t bytes_until_read_sampling_;
};
inline bool DBIter::ParseKey(ParsedInternalKey* ikey) {
Slice k = iter_->key();
size_t bytes_read = k.size() + iter_->value().size();
while (bytes_until_read_sampling_ < bytes_read) {
bytes_until_read_sampling_ += RandomCompactionPeriod();
db_->RecordReadSample(k);
}
assert(bytes_until_read_sampling_ >= bytes_read);
bytes_until_read_sampling_ -= bytes_read;
if (!ParseInternalKey(k, ikey)) {
status_ = Status::Corruption("corrupted internal key in DBIter");
return false;
} else {
return true;
}
}
void DBIter::Next() {
assert(valid_);
if (direction_ == kReverse) { // Switch directions?
direction_ = kForward;
// iter_ is pointing just before the entries for this->key(),
// so advance into the range of entries for this->key() and then
// use the normal skipping code below.
if (!iter_->Valid()) {
iter_->SeekToFirst();
} else {
iter_->Next();
}
if (!iter_->Valid()) {
valid_ = false;
saved_key_.clear();
return;
}
// saved_key_ already contains the key to skip past.
} else {
// Store in saved_key_ the current key so we skip it below.
SaveKey(ExtractUserKey(iter_->key()), &saved_key_);
// iter_ is pointing to current key. We can now safely move to the next to
// avoid checking current key.
iter_->Next();
if (!iter_->Valid()) {
valid_ = false;
saved_key_.clear();
return;
}
}
FindNextUserEntry(true, &saved_key_);
}
void DBIter::FindNextUserEntry(bool skipping, std::string* skip) {
// Loop until we hit an acceptable entry to yield
assert(iter_->Valid());
assert(direction_ == kForward);
do {
ParsedInternalKey ikey;
if (ParseKey(&ikey) && ikey.sequence <= sequence_) {
switch (ikey.type) {
case kTypeDeletion:
// Arrange to skip all upcoming entries for this key since
// they are hidden by this deletion.
SaveKey(ikey.user_key, skip);
skipping = true;
break;
case kTypeValue:
if (skipping &&
user_comparator_->Compare(ikey.user_key, *skip) <= 0) {
// Entry hidden
} else {
valid_ = true;
saved_key_.clear();
return;
}
break;
}
}
iter_->Next();
} while (iter_->Valid());
saved_key_.clear();
valid_ = false;
}
void DBIter::Prev() {
assert(valid_);
if (direction_ == kForward) { // Switch directions?
// iter_ is pointing at the current entry. Scan backwards until
// the key changes so we can use the normal reverse scanning code.
assert(iter_->Valid()); // Otherwise valid_ would have been false
SaveKey(ExtractUserKey(iter_->key()), &saved_key_);
while (true) {
iter_->Prev();
if (!iter_->Valid()) {
valid_ = false;
saved_key_.clear();
ClearSavedValue();
return;
}
if (user_comparator_->Compare(ExtractUserKey(iter_->key()), saved_key_) <
0) {
break;
}
}
direction_ = kReverse;
}
FindPrevUserEntry();
}
void DBIter::FindPrevUserEntry() {
assert(direction_ == kReverse);
ValueType value_type = kTypeDeletion;
if (iter_->Valid()) {
do {
ParsedInternalKey ikey;
if (ParseKey(&ikey) && ikey.sequence <= sequence_) {
if ((value_type != kTypeDeletion) &&
user_comparator_->Compare(ikey.user_key, saved_key_) < 0) {
// We encountered a non-deleted value in entries for previous keys,
break;
}
value_type = ikey.type;
if (value_type == kTypeDeletion) {
saved_key_.clear();
ClearSavedValue();
} else {
Slice raw_value = iter_->value();
if (saved_value_.capacity() > raw_value.size() + 1048576) {
std::string empty;
swap(empty, saved_value_);
}
SaveKey(ExtractUserKey(iter_->key()), &saved_key_);
saved_value_.assign(raw_value.data(), raw_value.size());
}
}
iter_->Prev();
} while (iter_->Valid());
}
if (value_type == kTypeDeletion) {
// End
valid_ = false;
saved_key_.clear();
ClearSavedValue();
direction_ = kForward;
} else {
valid_ = true;
}
}
void DBIter::Seek(const Slice& target) {
direction_ = kForward;
ClearSavedValue();
saved_key_.clear();
AppendInternalKey(&saved_key_,
ParsedInternalKey(target, sequence_, kValueTypeForSeek));
iter_->Seek(saved_key_);
if (iter_->Valid()) {
FindNextUserEntry(false, &saved_key_ /* temporary storage */);
} else {
valid_ = false;
}
}
void DBIter::SeekToFirst() {
direction_ = kForward;
ClearSavedValue();
iter_->SeekToFirst();
if (iter_->Valid()) {
FindNextUserEntry(false, &saved_key_ /* temporary storage */);
} else {
valid_ = false;
}
}
void DBIter::SeekToLast() {
direction_ = kReverse;
ClearSavedValue();
iter_->SeekToLast();
FindPrevUserEntry();
}
} // anonymous namespace
Iterator* NewDBIterator(DBImpl* db, const Comparator* user_key_comparator,
Iterator* internal_iter, SequenceNumber sequence,
uint32_t seed) {
return new DBIter(db, user_key_comparator, internal_iter, sequence, seed);
}
} // namespace leveldb
@@ -0,0 +1,26 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_DB_DB_ITER_H_
#define STORAGE_LEVELDB_DB_DB_ITER_H_
#include <cstdint>
#include "db/dbformat.h"
#include "leveldb/db.h"
namespace leveldb {
class DBImpl;
// Return a new iterator that converts internal keys (yielded by
// "*internal_iter") that were live at the specified "sequence" number
// into appropriate user keys.
Iterator* NewDBIterator(DBImpl* db, const Comparator* user_key_comparator,
Iterator* internal_iter, SequenceNumber sequence,
uint32_t seed);
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_DB_ITER_H_
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,136 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/dbformat.h"
#include <cstdio>
#include <sstream>
#include "port/port.h"
#include "util/coding.h"
namespace leveldb {
static uint64_t PackSequenceAndType(uint64_t seq, ValueType t) {
assert(seq <= kMaxSequenceNumber);
assert(t <= kValueTypeForSeek);
return (seq << 8) | t;
}
void AppendInternalKey(std::string* result, const ParsedInternalKey& key) {
result->append(key.user_key.data(), key.user_key.size());
PutFixed64(result, PackSequenceAndType(key.sequence, key.type));
}
std::string ParsedInternalKey::DebugString() const {
std::ostringstream ss;
ss << '\'' << EscapeString(user_key.ToString()) << "' @ " << sequence << " : "
<< static_cast<int>(type);
return ss.str();
}
std::string InternalKey::DebugString() const {
ParsedInternalKey parsed;
if (ParseInternalKey(rep_, &parsed)) {
return parsed.DebugString();
}
std::ostringstream ss;
ss << "(bad)" << EscapeString(rep_);
return ss.str();
}
const char* InternalKeyComparator::Name() const {
return "leveldb.InternalKeyComparator";
}
int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const {
// Order by:
// increasing user key (according to user-supplied comparator)
// decreasing sequence number
// decreasing type (though sequence# should be enough to disambiguate)
int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey));
if (r == 0) {
const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);
const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);
if (anum > bnum) {
r = -1;
} else if (anum < bnum) {
r = +1;
}
}
return r;
}
void InternalKeyComparator::FindShortestSeparator(std::string* start,
const Slice& limit) const {
// Attempt to shorten the user portion of the key
Slice user_start = ExtractUserKey(*start);
Slice user_limit = ExtractUserKey(limit);
std::string tmp(user_start.data(), user_start.size());
user_comparator_->FindShortestSeparator(&tmp, user_limit);
if (tmp.size() < user_start.size() &&
user_comparator_->Compare(user_start, tmp) < 0) {
// User key has become shorter physically, but larger logically.
// Tack on the earliest possible number to the shortened user key.
PutFixed64(&tmp,
PackSequenceAndType(kMaxSequenceNumber, kValueTypeForSeek));
assert(this->Compare(*start, tmp) < 0);
assert(this->Compare(tmp, limit) < 0);
start->swap(tmp);
}
}
void InternalKeyComparator::FindShortSuccessor(std::string* key) const {
Slice user_key = ExtractUserKey(*key);
std::string tmp(user_key.data(), user_key.size());
user_comparator_->FindShortSuccessor(&tmp);
if (tmp.size() < user_key.size() &&
user_comparator_->Compare(user_key, tmp) < 0) {
// User key has become shorter physically, but larger logically.
// Tack on the earliest possible number to the shortened user key.
PutFixed64(&tmp,
PackSequenceAndType(kMaxSequenceNumber, kValueTypeForSeek));
assert(this->Compare(*key, tmp) < 0);
key->swap(tmp);
}
}
const char* InternalFilterPolicy::Name() const { return user_policy_->Name(); }
void InternalFilterPolicy::CreateFilter(const Slice* keys, int n,
std::string* dst) const {
// We rely on the fact that the code in table.cc does not mind us
// adjusting keys[].
Slice* mkey = const_cast<Slice*>(keys);
for (int i = 0; i < n; i++) {
mkey[i] = ExtractUserKey(keys[i]);
// TODO(sanjay): Suppress dups?
}
user_policy_->CreateFilter(keys, n, dst);
}
bool InternalFilterPolicy::KeyMayMatch(const Slice& key, const Slice& f) const {
return user_policy_->KeyMayMatch(ExtractUserKey(key), f);
}
LookupKey::LookupKey(const Slice& user_key, SequenceNumber s) {
size_t usize = user_key.size();
size_t needed = usize + 13; // A conservative estimate
char* dst;
if (needed <= sizeof(space_)) {
dst = space_;
} else {
dst = new char[needed];
}
start_ = dst;
dst = EncodeVarint32(dst, usize + 8);
kstart_ = dst;
std::memcpy(dst, user_key.data(), usize);
dst += usize;
EncodeFixed64(dst, PackSequenceAndType(s, kValueTypeForSeek));
dst += 8;
end_ = dst;
}
} // namespace leveldb
+224
View File
@@ -0,0 +1,224 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_DB_DBFORMAT_H_
#define STORAGE_LEVELDB_DB_DBFORMAT_H_
#include <cstddef>
#include <cstdint>
#include <string>
#include "leveldb/comparator.h"
#include "leveldb/db.h"
#include "leveldb/filter_policy.h"
#include "leveldb/slice.h"
#include "leveldb/table_builder.h"
#include "util/coding.h"
#include "util/logging.h"
namespace leveldb {
// Grouping of constants. We may want to make some of these
// parameters set via options.
namespace config {
static const int kNumLevels = 7;
// Level-0 compaction is started when we hit this many files.
static const int kL0_CompactionTrigger = 4;
// Soft limit on number of level-0 files. We slow down writes at this point.
static const int kL0_SlowdownWritesTrigger = 8;
// Maximum number of level-0 files. We stop writes at this point.
static const int kL0_StopWritesTrigger = 12;
// Maximum level to which a new compacted memtable is pushed if it
// does not create overlap. We try to push to level 2 to avoid the
// relatively expensive level 0=>1 compactions and to avoid some
// expensive manifest file operations. We do not push all the way to
// the largest level since that can generate a lot of wasted disk
// space if the same key space is being repeatedly overwritten.
static const int kMaxMemCompactLevel = 2;
// Approximate gap in bytes between samples of data read during iteration.
static const int kReadBytesPeriod = 1048576;
} // namespace config
class InternalKey;
// Value types encoded as the last component of internal keys.
// DO NOT CHANGE THESE ENUM VALUES: they are embedded in the on-disk
// data structures.
enum ValueType { kTypeDeletion = 0x0, kTypeValue = 0x1 };
// kValueTypeForSeek defines the ValueType that should be passed when
// constructing a ParsedInternalKey object for seeking to a particular
// sequence number (since we sort sequence numbers in decreasing order
// and the value type is embedded as the low 8 bits in the sequence
// number in internal keys, we need to use the highest-numbered
// ValueType, not the lowest).
static const ValueType kValueTypeForSeek = kTypeValue;
typedef uint64_t SequenceNumber;
// We leave eight bits empty at the bottom so a type and sequence#
// can be packed together into 64-bits.
static const SequenceNumber kMaxSequenceNumber = ((0x1ull << 56) - 1);
struct ParsedInternalKey {
Slice user_key;
SequenceNumber sequence;
ValueType type;
ParsedInternalKey() {} // Intentionally left uninitialized (for speed)
ParsedInternalKey(const Slice& u, const SequenceNumber& seq, ValueType t)
: user_key(u), sequence(seq), type(t) {}
std::string DebugString() const;
};
// Return the length of the encoding of "key".
inline size_t InternalKeyEncodingLength(const ParsedInternalKey& key) {
return key.user_key.size() + 8;
}
// Append the serialization of "key" to *result.
void AppendInternalKey(std::string* result, const ParsedInternalKey& key);
// Attempt to parse an internal key from "internal_key". On success,
// stores the parsed data in "*result", and returns true.
//
// On error, returns false, leaves "*result" in an undefined state.
bool ParseInternalKey(const Slice& internal_key, ParsedInternalKey* result);
// Returns the user key portion of an internal key.
inline Slice ExtractUserKey(const Slice& internal_key) {
assert(internal_key.size() >= 8);
return Slice(internal_key.data(), internal_key.size() - 8);
}
// A comparator for internal keys that uses a specified comparator for
// the user key portion and breaks ties by decreasing sequence number.
class InternalKeyComparator : public Comparator {
private:
const Comparator* user_comparator_;
public:
explicit InternalKeyComparator(const Comparator* c) : user_comparator_(c) {}
const char* Name() const override;
int Compare(const Slice& a, const Slice& b) const override;
void FindShortestSeparator(std::string* start,
const Slice& limit) const override;
void FindShortSuccessor(std::string* key) const override;
const Comparator* user_comparator() const { return user_comparator_; }
int Compare(const InternalKey& a, const InternalKey& b) const;
};
// Filter policy wrapper that converts from internal keys to user keys
class InternalFilterPolicy : public FilterPolicy {
private:
const FilterPolicy* const user_policy_;
public:
explicit InternalFilterPolicy(const FilterPolicy* p) : user_policy_(p) {}
const char* Name() const override;
void CreateFilter(const Slice* keys, int n, std::string* dst) const override;
bool KeyMayMatch(const Slice& key, const Slice& filter) const override;
};
// Modules in this directory should keep internal keys wrapped inside
// the following class instead of plain strings so that we do not
// incorrectly use string comparisons instead of an InternalKeyComparator.
class InternalKey {
private:
std::string rep_;
public:
InternalKey() {} // Leave rep_ as empty to indicate it is invalid
InternalKey(const Slice& user_key, SequenceNumber s, ValueType t) {
AppendInternalKey(&rep_, ParsedInternalKey(user_key, s, t));
}
bool DecodeFrom(const Slice& s) {
rep_.assign(s.data(), s.size());
return !rep_.empty();
}
Slice Encode() const {
assert(!rep_.empty());
return rep_;
}
Slice user_key() const { return ExtractUserKey(rep_); }
void SetFrom(const ParsedInternalKey& p) {
rep_.clear();
AppendInternalKey(&rep_, p);
}
void Clear() { rep_.clear(); }
std::string DebugString() const;
};
inline int InternalKeyComparator::Compare(const InternalKey& a,
const InternalKey& b) const {
return Compare(a.Encode(), b.Encode());
}
inline bool ParseInternalKey(const Slice& internal_key,
ParsedInternalKey* result) {
const size_t n = internal_key.size();
if (n < 8) return false;
uint64_t num = DecodeFixed64(internal_key.data() + n - 8);
uint8_t c = num & 0xff;
result->sequence = num >> 8;
result->type = static_cast<ValueType>(c);
result->user_key = Slice(internal_key.data(), n - 8);
return (c <= static_cast<uint8_t>(kTypeValue));
}
// A helper class useful for DBImpl::Get()
class LookupKey {
public:
// Initialize *this for looking up user_key at a snapshot with
// the specified sequence number.
LookupKey(const Slice& user_key, SequenceNumber sequence);
LookupKey(const LookupKey&) = delete;
LookupKey& operator=(const LookupKey&) = delete;
~LookupKey();
// Return a key suitable for lookup in a MemTable.
Slice memtable_key() const { return Slice(start_, end_ - start_); }
// Return an internal key (suitable for passing to an internal iterator)
Slice internal_key() const { return Slice(kstart_, end_ - kstart_); }
// Return the user key
Slice user_key() const { return Slice(kstart_, end_ - kstart_ - 8); }
private:
// We construct a char array of the form:
// klength varint32 <-- start_
// userkey char[klength] <-- kstart_
// tag uint64
// <-- end_
// The array is a suitable MemTable key.
// The suffix starting with "userkey" can be used as an InternalKey.
const char* start_;
const char* kstart_;
const char* end_;
char space_[200]; // Avoid allocation for short keys
};
inline LookupKey::~LookupKey() {
if (start_ != space_) delete[] start_;
}
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_DBFORMAT_H_
@@ -0,0 +1,128 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/dbformat.h"
#include "gtest/gtest.h"
#include "util/logging.h"
namespace leveldb {
static std::string IKey(const std::string& user_key, uint64_t seq,
ValueType vt) {
std::string encoded;
AppendInternalKey(&encoded, ParsedInternalKey(user_key, seq, vt));
return encoded;
}
static std::string Shorten(const std::string& s, const std::string& l) {
std::string result = s;
InternalKeyComparator(BytewiseComparator()).FindShortestSeparator(&result, l);
return result;
}
static std::string ShortSuccessor(const std::string& s) {
std::string result = s;
InternalKeyComparator(BytewiseComparator()).FindShortSuccessor(&result);
return result;
}
static void TestKey(const std::string& key, uint64_t seq, ValueType vt) {
std::string encoded = IKey(key, seq, vt);
Slice in(encoded);
ParsedInternalKey decoded("", 0, kTypeValue);
ASSERT_TRUE(ParseInternalKey(in, &decoded));
ASSERT_EQ(key, decoded.user_key.ToString());
ASSERT_EQ(seq, decoded.sequence);
ASSERT_EQ(vt, decoded.type);
ASSERT_TRUE(!ParseInternalKey(Slice("bar"), &decoded));
}
TEST(FormatTest, InternalKey_EncodeDecode) {
const char* keys[] = {"", "k", "hello", "longggggggggggggggggggggg"};
const uint64_t seq[] = {1,
2,
3,
(1ull << 8) - 1,
1ull << 8,
(1ull << 8) + 1,
(1ull << 16) - 1,
1ull << 16,
(1ull << 16) + 1,
(1ull << 32) - 1,
1ull << 32,
(1ull << 32) + 1};
for (int k = 0; k < sizeof(keys) / sizeof(keys[0]); k++) {
for (int s = 0; s < sizeof(seq) / sizeof(seq[0]); s++) {
TestKey(keys[k], seq[s], kTypeValue);
TestKey("hello", 1, kTypeDeletion);
}
}
}
TEST(FormatTest, InternalKey_DecodeFromEmpty) {
InternalKey internal_key;
ASSERT_TRUE(!internal_key.DecodeFrom(""));
}
TEST(FormatTest, InternalKeyShortSeparator) {
// When user keys are same
ASSERT_EQ(IKey("foo", 100, kTypeValue),
Shorten(IKey("foo", 100, kTypeValue), IKey("foo", 99, kTypeValue)));
ASSERT_EQ(
IKey("foo", 100, kTypeValue),
Shorten(IKey("foo", 100, kTypeValue), IKey("foo", 101, kTypeValue)));
ASSERT_EQ(
IKey("foo", 100, kTypeValue),
Shorten(IKey("foo", 100, kTypeValue), IKey("foo", 100, kTypeValue)));
ASSERT_EQ(
IKey("foo", 100, kTypeValue),
Shorten(IKey("foo", 100, kTypeValue), IKey("foo", 100, kTypeDeletion)));
// When user keys are misordered
ASSERT_EQ(IKey("foo", 100, kTypeValue),
Shorten(IKey("foo", 100, kTypeValue), IKey("bar", 99, kTypeValue)));
// When user keys are different, but correctly ordered
ASSERT_EQ(
IKey("g", kMaxSequenceNumber, kValueTypeForSeek),
Shorten(IKey("foo", 100, kTypeValue), IKey("hello", 200, kTypeValue)));
// When start user key is prefix of limit user key
ASSERT_EQ(
IKey("foo", 100, kTypeValue),
Shorten(IKey("foo", 100, kTypeValue), IKey("foobar", 200, kTypeValue)));
// When limit user key is prefix of start user key
ASSERT_EQ(
IKey("foobar", 100, kTypeValue),
Shorten(IKey("foobar", 100, kTypeValue), IKey("foo", 200, kTypeValue)));
}
TEST(FormatTest, InternalKeyShortestSuccessor) {
ASSERT_EQ(IKey("g", kMaxSequenceNumber, kValueTypeForSeek),
ShortSuccessor(IKey("foo", 100, kTypeValue)));
ASSERT_EQ(IKey("\xff\xff", 100, kTypeValue),
ShortSuccessor(IKey("\xff\xff", 100, kTypeValue)));
}
TEST(FormatTest, ParsedInternalKeyDebugString) {
ParsedInternalKey key("The \"key\" in 'single quotes'", 42, kTypeValue);
ASSERT_EQ("'The \"key\" in 'single quotes'' @ 42 : 1", key.DebugString());
}
TEST(FormatTest, InternalKeyDebugString) {
InternalKey key("The \"key\" in 'single quotes'", 42, kTypeValue);
ASSERT_EQ("'The \"key\" in 'single quotes'' @ 42 : 1", key.DebugString());
InternalKey invalid_key;
ASSERT_EQ("(bad)", invalid_key.DebugString());
}
} // namespace leveldb
@@ -0,0 +1,232 @@
// Copyright (c) 2012 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "leveldb/dumpfile.h"
#include <cstdio>
#include "db/dbformat.h"
#include "db/filename.h"
#include "db/log_reader.h"
#include "db/version_edit.h"
#include "db/write_batch_internal.h"
#include "leveldb/env.h"
#include "leveldb/iterator.h"
#include "leveldb/options.h"
#include "leveldb/status.h"
#include "leveldb/table.h"
#include "leveldb/write_batch.h"
#include "util/logging.h"
namespace leveldb {
namespace {
bool GuessType(const std::string& fname, FileType* type) {
size_t pos = fname.rfind('/');
std::string basename;
if (pos == std::string::npos) {
basename = fname;
} else {
basename = std::string(fname.data() + pos + 1, fname.size() - pos - 1);
}
uint64_t ignored;
return ParseFileName(basename, &ignored, type);
}
// Notified when log reader encounters corruption.
class CorruptionReporter : public log::Reader::Reporter {
public:
void Corruption(size_t bytes, const Status& status) override {
std::string r = "corruption: ";
AppendNumberTo(&r, bytes);
r += " bytes; ";
r += status.ToString();
r.push_back('\n');
dst_->Append(r);
}
WritableFile* dst_;
};
// Print contents of a log file. (*func)() is called on every record.
Status PrintLogContents(Env* env, const std::string& fname,
void (*func)(uint64_t, Slice, WritableFile*),
WritableFile* dst) {
SequentialFile* file;
Status s = env->NewSequentialFile(fname, &file);
if (!s.ok()) {
return s;
}
CorruptionReporter reporter;
reporter.dst_ = dst;
log::Reader reader(file, &reporter, true, 0);
Slice record;
std::string scratch;
while (reader.ReadRecord(&record, &scratch)) {
(*func)(reader.LastRecordOffset(), record, dst);
}
delete file;
return Status::OK();
}
// Called on every item found in a WriteBatch.
class WriteBatchItemPrinter : public WriteBatch::Handler {
public:
void Put(const Slice& key, const Slice& value) override {
std::string r = " put '";
AppendEscapedStringTo(&r, key);
r += "' '";
AppendEscapedStringTo(&r, value);
r += "'\n";
dst_->Append(r);
}
void Delete(const Slice& key) override {
std::string r = " del '";
AppendEscapedStringTo(&r, key);
r += "'\n";
dst_->Append(r);
}
WritableFile* dst_;
};
// Called on every log record (each one of which is a WriteBatch)
// found in a kLogFile.
static void WriteBatchPrinter(uint64_t pos, Slice record, WritableFile* dst) {
std::string r = "--- offset ";
AppendNumberTo(&r, pos);
r += "; ";
if (record.size() < 12) {
r += "log record length ";
AppendNumberTo(&r, record.size());
r += " is too small\n";
dst->Append(r);
return;
}
WriteBatch batch;
WriteBatchInternal::SetContents(&batch, record);
r += "sequence ";
AppendNumberTo(&r, WriteBatchInternal::Sequence(&batch));
r.push_back('\n');
dst->Append(r);
WriteBatchItemPrinter batch_item_printer;
batch_item_printer.dst_ = dst;
Status s = batch.Iterate(&batch_item_printer);
if (!s.ok()) {
dst->Append(" error: " + s.ToString() + "\n");
}
}
Status DumpLog(Env* env, const std::string& fname, WritableFile* dst) {
return PrintLogContents(env, fname, WriteBatchPrinter, dst);
}
// Called on every log record (each one of which is a WriteBatch)
// found in a kDescriptorFile.
static void VersionEditPrinter(uint64_t pos, Slice record, WritableFile* dst) {
std::string r = "--- offset ";
AppendNumberTo(&r, pos);
r += "; ";
VersionEdit edit;
Status s = edit.DecodeFrom(record);
if (!s.ok()) {
r += s.ToString();
r.push_back('\n');
} else {
r += edit.DebugString();
}
dst->Append(r);
}
Status DumpDescriptor(Env* env, const std::string& fname, WritableFile* dst) {
return PrintLogContents(env, fname, VersionEditPrinter, dst);
}
Status DumpTable(Env* env, const std::string& fname, WritableFile* dst) {
uint64_t file_size;
RandomAccessFile* file = nullptr;
Table* table = nullptr;
Status s = env->GetFileSize(fname, &file_size);
if (s.ok()) {
s = env->NewRandomAccessFile(fname, &file);
}
if (s.ok()) {
// We use the default comparator, which may or may not match the
// comparator used in this database. However this should not cause
// problems since we only use Table operations that do not require
// any comparisons. In particular, we do not call Seek or Prev.
s = Table::Open(Options(), file, file_size, &table);
}
if (!s.ok()) {
delete table;
delete file;
return s;
}
ReadOptions ro;
ro.fill_cache = false;
Iterator* iter = table->NewIterator(ro);
std::string r;
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
r.clear();
ParsedInternalKey key;
if (!ParseInternalKey(iter->key(), &key)) {
r = "badkey '";
AppendEscapedStringTo(&r, iter->key());
r += "' => '";
AppendEscapedStringTo(&r, iter->value());
r += "'\n";
dst->Append(r);
} else {
r = "'";
AppendEscapedStringTo(&r, key.user_key);
r += "' @ ";
AppendNumberTo(&r, key.sequence);
r += " : ";
if (key.type == kTypeDeletion) {
r += "del";
} else if (key.type == kTypeValue) {
r += "val";
} else {
AppendNumberTo(&r, key.type);
}
r += " => '";
AppendEscapedStringTo(&r, iter->value());
r += "'\n";
dst->Append(r);
}
}
s = iter->status();
if (!s.ok()) {
dst->Append("iterator error: " + s.ToString() + "\n");
}
delete iter;
delete table;
delete file;
return Status::OK();
}
} // namespace
Status DumpFile(Env* env, const std::string& fname, WritableFile* dst) {
FileType ftype;
if (!GuessType(fname, &ftype)) {
return Status::InvalidArgument(fname + ": unknown file type");
}
switch (ftype) {
case kLogFile:
return DumpLog(env, fname, dst);
case kDescriptorFile:
return DumpDescriptor(env, fname, dst);
case kTableFile:
return DumpTable(env, fname, dst);
default:
break;
}
return Status::InvalidArgument(fname + ": not a dump-able file type");
}
} // namespace leveldb
@@ -0,0 +1,550 @@
// Copyright 2014 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
// This test uses a custom Env to keep track of the state of a filesystem as of
// the last "sync". It then checks for data loss errors by purposely dropping
// file data (or entire files) not protected by a "sync".
#include <map>
#include <set>
#include "gtest/gtest.h"
#include "db/db_impl.h"
#include "db/filename.h"
#include "db/log_format.h"
#include "db/version_set.h"
#include "leveldb/cache.h"
#include "leveldb/db.h"
#include "leveldb/env.h"
#include "leveldb/table.h"
#include "leveldb/write_batch.h"
#include "port/port.h"
#include "port/thread_annotations.h"
#include "util/logging.h"
#include "util/mutexlock.h"
#include "util/testutil.h"
namespace leveldb {
static const int kValueSize = 1000;
static const int kMaxNumValues = 2000;
static const size_t kNumIterations = 3;
class FaultInjectionTestEnv;
namespace {
// Assume a filename, and not a directory name like "/foo/bar/"
static std::string GetDirName(const std::string& filename) {
size_t found = filename.find_last_of("/\\");
if (found == std::string::npos) {
return "";
} else {
return filename.substr(0, found);
}
}
Status SyncDir(const std::string& dir) {
// As this is a test it isn't required to *actually* sync this directory.
return Status::OK();
}
// A basic file truncation function suitable for this test.
Status Truncate(const std::string& filename, uint64_t length) {
leveldb::Env* env = leveldb::Env::Default();
SequentialFile* orig_file;
Status s = env->NewSequentialFile(filename, &orig_file);
if (!s.ok()) return s;
char* scratch = new char[length];
leveldb::Slice result;
s = orig_file->Read(length, &result, scratch);
delete orig_file;
if (s.ok()) {
std::string tmp_name = GetDirName(filename) + "/truncate.tmp";
WritableFile* tmp_file;
s = env->NewWritableFile(tmp_name, &tmp_file);
if (s.ok()) {
s = tmp_file->Append(result);
delete tmp_file;
if (s.ok()) {
s = env->RenameFile(tmp_name, filename);
} else {
env->RemoveFile(tmp_name);
}
}
}
delete[] scratch;
return s;
}
struct FileState {
std::string filename_;
int64_t pos_;
int64_t pos_at_last_sync_;
int64_t pos_at_last_flush_;
FileState(const std::string& filename)
: filename_(filename),
pos_(-1),
pos_at_last_sync_(-1),
pos_at_last_flush_(-1) {}
FileState() : pos_(-1), pos_at_last_sync_(-1), pos_at_last_flush_(-1) {}
bool IsFullySynced() const { return pos_ <= 0 || pos_ == pos_at_last_sync_; }
Status DropUnsyncedData() const;
};
} // anonymous namespace
// A wrapper around WritableFile which informs another Env whenever this file
// is written to or sync'ed.
class TestWritableFile : public WritableFile {
public:
TestWritableFile(const FileState& state, WritableFile* f,
FaultInjectionTestEnv* env);
~TestWritableFile() override;
Status Append(const Slice& data) override;
Status Close() override;
Status Flush() override;
Status Sync() override;
private:
FileState state_;
WritableFile* target_;
bool writable_file_opened_;
FaultInjectionTestEnv* env_;
Status SyncParent();
};
class FaultInjectionTestEnv : public EnvWrapper {
public:
FaultInjectionTestEnv()
: EnvWrapper(Env::Default()), filesystem_active_(true) {}
~FaultInjectionTestEnv() override = default;
Status NewWritableFile(const std::string& fname,
WritableFile** result) override;
Status NewAppendableFile(const std::string& fname,
WritableFile** result) override;
Status RemoveFile(const std::string& f) override;
Status RenameFile(const std::string& s, const std::string& t) override;
void WritableFileClosed(const FileState& state);
Status DropUnsyncedFileData();
Status RemoveFilesCreatedAfterLastDirSync();
void DirWasSynced();
bool IsFileCreatedSinceLastDirSync(const std::string& filename);
void ResetState();
void UntrackFile(const std::string& f);
// Setting the filesystem to inactive is the test equivalent to simulating a
// system reset. Setting to inactive will freeze our saved filesystem state so
// that it will stop being recorded. It can then be reset back to the state at
// the time of the reset.
bool IsFilesystemActive() LOCKS_EXCLUDED(mutex_) {
MutexLock l(&mutex_);
return filesystem_active_;
}
void SetFilesystemActive(bool active) LOCKS_EXCLUDED(mutex_) {
MutexLock l(&mutex_);
filesystem_active_ = active;
}
private:
port::Mutex mutex_;
std::map<std::string, FileState> db_file_state_ GUARDED_BY(mutex_);
std::set<std::string> new_files_since_last_dir_sync_ GUARDED_BY(mutex_);
bool filesystem_active_ GUARDED_BY(mutex_); // Record flushes, syncs, writes
};
TestWritableFile::TestWritableFile(const FileState& state, WritableFile* f,
FaultInjectionTestEnv* env)
: state_(state), target_(f), writable_file_opened_(true), env_(env) {
assert(f != nullptr);
}
TestWritableFile::~TestWritableFile() {
if (writable_file_opened_) {
Close();
}
delete target_;
}
Status TestWritableFile::Append(const Slice& data) {
Status s = target_->Append(data);
if (s.ok() && env_->IsFilesystemActive()) {
state_.pos_ += data.size();
}
return s;
}
Status TestWritableFile::Close() {
writable_file_opened_ = false;
Status s = target_->Close();
if (s.ok()) {
env_->WritableFileClosed(state_);
}
return s;
}
Status TestWritableFile::Flush() {
Status s = target_->Flush();
if (s.ok() && env_->IsFilesystemActive()) {
state_.pos_at_last_flush_ = state_.pos_;
}
return s;
}
Status TestWritableFile::SyncParent() {
Status s = SyncDir(GetDirName(state_.filename_));
if (s.ok()) {
env_->DirWasSynced();
}
return s;
}
Status TestWritableFile::Sync() {
if (!env_->IsFilesystemActive()) {
return Status::OK();
}
// Ensure new files referred to by the manifest are in the filesystem.
Status s = target_->Sync();
if (s.ok()) {
state_.pos_at_last_sync_ = state_.pos_;
}
if (env_->IsFileCreatedSinceLastDirSync(state_.filename_)) {
Status ps = SyncParent();
if (s.ok() && !ps.ok()) {
s = ps;
}
}
return s;
}
Status FaultInjectionTestEnv::NewWritableFile(const std::string& fname,
WritableFile** result) {
WritableFile* actual_writable_file;
Status s = target()->NewWritableFile(fname, &actual_writable_file);
if (s.ok()) {
FileState state(fname);
state.pos_ = 0;
*result = new TestWritableFile(state, actual_writable_file, this);
// NewWritableFile doesn't append to files, so if the same file is
// opened again then it will be truncated - so forget our saved
// state.
UntrackFile(fname);
MutexLock l(&mutex_);
new_files_since_last_dir_sync_.insert(fname);
}
return s;
}
Status FaultInjectionTestEnv::NewAppendableFile(const std::string& fname,
WritableFile** result) {
WritableFile* actual_writable_file;
Status s = target()->NewAppendableFile(fname, &actual_writable_file);
if (s.ok()) {
FileState state(fname);
state.pos_ = 0;
{
MutexLock l(&mutex_);
if (db_file_state_.count(fname) == 0) {
new_files_since_last_dir_sync_.insert(fname);
} else {
state = db_file_state_[fname];
}
}
*result = new TestWritableFile(state, actual_writable_file, this);
}
return s;
}
Status FaultInjectionTestEnv::DropUnsyncedFileData() {
Status s;
MutexLock l(&mutex_);
for (const auto& kvp : db_file_state_) {
if (!s.ok()) {
break;
}
const FileState& state = kvp.second;
if (!state.IsFullySynced()) {
s = state.DropUnsyncedData();
}
}
return s;
}
void FaultInjectionTestEnv::DirWasSynced() {
MutexLock l(&mutex_);
new_files_since_last_dir_sync_.clear();
}
bool FaultInjectionTestEnv::IsFileCreatedSinceLastDirSync(
const std::string& filename) {
MutexLock l(&mutex_);
return new_files_since_last_dir_sync_.find(filename) !=
new_files_since_last_dir_sync_.end();
}
void FaultInjectionTestEnv::UntrackFile(const std::string& f) {
MutexLock l(&mutex_);
db_file_state_.erase(f);
new_files_since_last_dir_sync_.erase(f);
}
Status FaultInjectionTestEnv::RemoveFile(const std::string& f) {
Status s = EnvWrapper::RemoveFile(f);
EXPECT_LEVELDB_OK(s);
if (s.ok()) {
UntrackFile(f);
}
return s;
}
Status FaultInjectionTestEnv::RenameFile(const std::string& s,
const std::string& t) {
Status ret = EnvWrapper::RenameFile(s, t);
if (ret.ok()) {
MutexLock l(&mutex_);
if (db_file_state_.find(s) != db_file_state_.end()) {
db_file_state_[t] = db_file_state_[s];
db_file_state_.erase(s);
}
if (new_files_since_last_dir_sync_.erase(s) != 0) {
assert(new_files_since_last_dir_sync_.find(t) ==
new_files_since_last_dir_sync_.end());
new_files_since_last_dir_sync_.insert(t);
}
}
return ret;
}
void FaultInjectionTestEnv::ResetState() {
// Since we are not destroying the database, the existing files
// should keep their recorded synced/flushed state. Therefore
// we do not reset db_file_state_ and new_files_since_last_dir_sync_.
SetFilesystemActive(true);
}
Status FaultInjectionTestEnv::RemoveFilesCreatedAfterLastDirSync() {
// Because RemoveFile access this container make a copy to avoid deadlock
mutex_.Lock();
std::set<std::string> new_files(new_files_since_last_dir_sync_.begin(),
new_files_since_last_dir_sync_.end());
mutex_.Unlock();
Status status;
for (const auto& new_file : new_files) {
Status remove_status = RemoveFile(new_file);
if (!remove_status.ok() && status.ok()) {
status = std::move(remove_status);
}
}
return status;
}
void FaultInjectionTestEnv::WritableFileClosed(const FileState& state) {
MutexLock l(&mutex_);
db_file_state_[state.filename_] = state;
}
Status FileState::DropUnsyncedData() const {
int64_t sync_pos = pos_at_last_sync_ == -1 ? 0 : pos_at_last_sync_;
return Truncate(filename_, sync_pos);
}
class FaultInjectionTest : public testing::Test {
public:
enum ExpectedVerifResult { VAL_EXPECT_NO_ERROR, VAL_EXPECT_ERROR };
enum ResetMethod { RESET_DROP_UNSYNCED_DATA, RESET_DELETE_UNSYNCED_FILES };
FaultInjectionTestEnv* env_;
std::string dbname_;
Cache* tiny_cache_;
Options options_;
DB* db_;
FaultInjectionTest()
: env_(new FaultInjectionTestEnv),
tiny_cache_(NewLRUCache(100)),
db_(nullptr) {
dbname_ = testing::TempDir() + "fault_test";
DestroyDB(dbname_, Options()); // Destroy any db from earlier run
options_.reuse_logs = true;
options_.env = env_;
options_.paranoid_checks = true;
options_.block_cache = tiny_cache_;
options_.create_if_missing = true;
}
~FaultInjectionTest() {
CloseDB();
DestroyDB(dbname_, Options());
delete tiny_cache_;
delete env_;
}
void ReuseLogs(bool reuse) { options_.reuse_logs = reuse; }
void Build(int start_idx, int num_vals) {
std::string key_space, value_space;
WriteBatch batch;
for (int i = start_idx; i < start_idx + num_vals; i++) {
Slice key = Key(i, &key_space);
batch.Clear();
batch.Put(key, Value(i, &value_space));
WriteOptions options;
ASSERT_LEVELDB_OK(db_->Write(options, &batch));
}
}
Status ReadValue(int i, std::string* val) const {
std::string key_space, value_space;
Slice key = Key(i, &key_space);
Value(i, &value_space);
ReadOptions options;
return db_->Get(options, key, val);
}
Status Verify(int start_idx, int num_vals,
ExpectedVerifResult expected) const {
std::string val;
std::string value_space;
Status s;
for (int i = start_idx; i < start_idx + num_vals && s.ok(); i++) {
Value(i, &value_space);
s = ReadValue(i, &val);
if (expected == VAL_EXPECT_NO_ERROR) {
if (s.ok()) {
EXPECT_EQ(value_space, val);
}
} else if (s.ok()) {
std::fprintf(stderr, "Expected an error at %d, but was OK\n", i);
s = Status::IOError(dbname_, "Expected value error:");
} else {
s = Status::OK(); // An expected error
}
}
return s;
}
// Return the ith key
Slice Key(int i, std::string* storage) const {
char buf[100];
std::snprintf(buf, sizeof(buf), "%016d", i);
storage->assign(buf, strlen(buf));
return Slice(*storage);
}
// Return the value to associate with the specified key
Slice Value(int k, std::string* storage) const {
Random r(k);
return test::RandomString(&r, kValueSize, storage);
}
Status OpenDB() {
delete db_;
db_ = nullptr;
env_->ResetState();
return DB::Open(options_, dbname_, &db_);
}
void CloseDB() {
delete db_;
db_ = nullptr;
}
void DeleteAllData() {
Iterator* iter = db_->NewIterator(ReadOptions());
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
ASSERT_LEVELDB_OK(db_->Delete(WriteOptions(), iter->key()));
}
delete iter;
}
void ResetDBState(ResetMethod reset_method) {
switch (reset_method) {
case RESET_DROP_UNSYNCED_DATA:
ASSERT_LEVELDB_OK(env_->DropUnsyncedFileData());
break;
case RESET_DELETE_UNSYNCED_FILES:
ASSERT_LEVELDB_OK(env_->RemoveFilesCreatedAfterLastDirSync());
break;
default:
assert(false);
}
}
void PartialCompactTestPreFault(int num_pre_sync, int num_post_sync) {
DeleteAllData();
Build(0, num_pre_sync);
db_->CompactRange(nullptr, nullptr);
Build(num_pre_sync, num_post_sync);
}
void PartialCompactTestReopenWithFault(ResetMethod reset_method,
int num_pre_sync, int num_post_sync) {
env_->SetFilesystemActive(false);
CloseDB();
ResetDBState(reset_method);
ASSERT_LEVELDB_OK(OpenDB());
ASSERT_LEVELDB_OK(
Verify(0, num_pre_sync, FaultInjectionTest::VAL_EXPECT_NO_ERROR));
ASSERT_LEVELDB_OK(Verify(num_pre_sync, num_post_sync,
FaultInjectionTest::VAL_EXPECT_ERROR));
}
void NoWriteTestPreFault() {}
void NoWriteTestReopenWithFault(ResetMethod reset_method) {
CloseDB();
ResetDBState(reset_method);
ASSERT_LEVELDB_OK(OpenDB());
}
void DoTest() {
Random rnd(0);
ASSERT_LEVELDB_OK(OpenDB());
for (size_t idx = 0; idx < kNumIterations; idx++) {
int num_pre_sync = rnd.Uniform(kMaxNumValues);
int num_post_sync = rnd.Uniform(kMaxNumValues);
PartialCompactTestPreFault(num_pre_sync, num_post_sync);
PartialCompactTestReopenWithFault(RESET_DROP_UNSYNCED_DATA, num_pre_sync,
num_post_sync);
NoWriteTestPreFault();
NoWriteTestReopenWithFault(RESET_DROP_UNSYNCED_DATA);
PartialCompactTestPreFault(num_pre_sync, num_post_sync);
// No new files created so we expect all values since no files will be
// dropped.
PartialCompactTestReopenWithFault(RESET_DELETE_UNSYNCED_FILES,
num_pre_sync + num_post_sync, 0);
NoWriteTestPreFault();
NoWriteTestReopenWithFault(RESET_DELETE_UNSYNCED_FILES);
}
}
};
TEST_F(FaultInjectionTest, FaultTestNoLogReuse) {
ReuseLogs(false);
DoTest();
}
TEST_F(FaultInjectionTest, FaultTestWithLogReuse) {
ReuseLogs(true);
DoTest();
}
} // namespace leveldb
@@ -0,0 +1,141 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/filename.h"
#include <cassert>
#include <cstdio>
#include "db/dbformat.h"
#include "leveldb/env.h"
#include "util/logging.h"
namespace leveldb {
// A utility routine: write "data" to the named file and Sync() it.
Status WriteStringToFileSync(Env* env, const Slice& data,
const std::string& fname);
static std::string MakeFileName(const std::string& dbname, uint64_t number,
const char* suffix) {
char buf[100];
std::snprintf(buf, sizeof(buf), "/%06llu.%s",
static_cast<unsigned long long>(number), suffix);
return dbname + buf;
}
std::string LogFileName(const std::string& dbname, uint64_t number) {
assert(number > 0);
return MakeFileName(dbname, number, "log");
}
std::string TableFileName(const std::string& dbname, uint64_t number) {
assert(number > 0);
return MakeFileName(dbname, number, "ldb");
}
std::string SSTTableFileName(const std::string& dbname, uint64_t number) {
assert(number > 0);
return MakeFileName(dbname, number, "sst");
}
std::string DescriptorFileName(const std::string& dbname, uint64_t number) {
assert(number > 0);
char buf[100];
std::snprintf(buf, sizeof(buf), "/MANIFEST-%06llu",
static_cast<unsigned long long>(number));
return dbname + buf;
}
std::string CurrentFileName(const std::string& dbname) {
return dbname + "/CURRENT";
}
std::string LockFileName(const std::string& dbname) { return dbname + "/LOCK"; }
std::string TempFileName(const std::string& dbname, uint64_t number) {
assert(number > 0);
return MakeFileName(dbname, number, "dbtmp");
}
std::string InfoLogFileName(const std::string& dbname) {
return dbname + "/LOG";
}
// Return the name of the old info log file for "dbname".
std::string OldInfoLogFileName(const std::string& dbname) {
return dbname + "/LOG.old";
}
// Owned filenames have the form:
// dbname/CURRENT
// dbname/LOCK
// dbname/LOG
// dbname/LOG.old
// dbname/MANIFEST-[0-9]+
// dbname/[0-9]+.(log|sst|ldb)
bool ParseFileName(const std::string& filename, uint64_t* number,
FileType* type) {
Slice rest(filename);
if (rest == "CURRENT") {
*number = 0;
*type = kCurrentFile;
} else if (rest == "LOCK") {
*number = 0;
*type = kDBLockFile;
} else if (rest == "LOG" || rest == "LOG.old") {
*number = 0;
*type = kInfoLogFile;
} else if (rest.starts_with("MANIFEST-")) {
rest.remove_prefix(strlen("MANIFEST-"));
uint64_t num;
if (!ConsumeDecimalNumber(&rest, &num)) {
return false;
}
if (!rest.empty()) {
return false;
}
*type = kDescriptorFile;
*number = num;
} else {
// Avoid strtoull() to keep filename format independent of the
// current locale
uint64_t num;
if (!ConsumeDecimalNumber(&rest, &num)) {
return false;
}
Slice suffix = rest;
if (suffix == Slice(".log")) {
*type = kLogFile;
} else if (suffix == Slice(".sst") || suffix == Slice(".ldb")) {
*type = kTableFile;
} else if (suffix == Slice(".dbtmp")) {
*type = kTempFile;
} else {
return false;
}
*number = num;
}
return true;
}
Status SetCurrentFile(Env* env, const std::string& dbname,
uint64_t descriptor_number) {
// Remove leading "dbname/" and add newline to manifest file name
std::string manifest = DescriptorFileName(dbname, descriptor_number);
Slice contents = manifest;
assert(contents.starts_with(dbname + "/"));
contents.remove_prefix(dbname.size() + 1);
std::string tmp = TempFileName(dbname, descriptor_number);
Status s = WriteStringToFileSync(env, contents.ToString() + "\n", tmp);
if (s.ok()) {
s = env->RenameFile(tmp, CurrentFileName(dbname));
}
if (!s.ok()) {
env->RemoveFile(tmp);
}
return s;
}
} // namespace leveldb
@@ -0,0 +1,83 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// File names used by DB code
#ifndef STORAGE_LEVELDB_DB_FILENAME_H_
#define STORAGE_LEVELDB_DB_FILENAME_H_
#include <cstdint>
#include <string>
#include "leveldb/slice.h"
#include "leveldb/status.h"
#include "port/port.h"
namespace leveldb {
class Env;
enum FileType {
kLogFile,
kDBLockFile,
kTableFile,
kDescriptorFile,
kCurrentFile,
kTempFile,
kInfoLogFile // Either the current one, or an old one
};
// Return the name of the log file with the specified number
// in the db named by "dbname". The result will be prefixed with
// "dbname".
std::string LogFileName(const std::string& dbname, uint64_t number);
// Return the name of the sstable with the specified number
// in the db named by "dbname". The result will be prefixed with
// "dbname".
std::string TableFileName(const std::string& dbname, uint64_t number);
// Return the legacy file name for an sstable with the specified number
// in the db named by "dbname". The result will be prefixed with
// "dbname".
std::string SSTTableFileName(const std::string& dbname, uint64_t number);
// Return the name of the descriptor file for the db named by
// "dbname" and the specified incarnation number. The result will be
// prefixed with "dbname".
std::string DescriptorFileName(const std::string& dbname, uint64_t number);
// Return the name of the current file. This file contains the name
// of the current manifest file. The result will be prefixed with
// "dbname".
std::string CurrentFileName(const std::string& dbname);
// Return the name of the lock file for the db named by
// "dbname". The result will be prefixed with "dbname".
std::string LockFileName(const std::string& dbname);
// Return the name of a temporary file owned by the db named "dbname".
// The result will be prefixed with "dbname".
std::string TempFileName(const std::string& dbname, uint64_t number);
// Return the name of the info log file for "dbname".
std::string InfoLogFileName(const std::string& dbname);
// Return the name of the old info log file for "dbname".
std::string OldInfoLogFileName(const std::string& dbname);
// If filename is a leveldb file, store the type of the file in *type.
// The number encoded in the filename is stored in *number. If the
// filename was successfully parsed, returns true. Else return false.
bool ParseFileName(const std::string& filename, uint64_t* number,
FileType* type);
// Make the CURRENT file point to the descriptor file with the
// specified number.
Status SetCurrentFile(Env* env, const std::string& dbname,
uint64_t descriptor_number);
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_FILENAME_H_
@@ -0,0 +1,127 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/filename.h"
#include "gtest/gtest.h"
#include "db/dbformat.h"
#include "port/port.h"
#include "util/logging.h"
namespace leveldb {
TEST(FileNameTest, Parse) {
Slice db;
FileType type;
uint64_t number;
// Successful parses
static struct {
const char* fname;
uint64_t number;
FileType type;
} cases[] = {
{"100.log", 100, kLogFile},
{"0.log", 0, kLogFile},
{"0.sst", 0, kTableFile},
{"0.ldb", 0, kTableFile},
{"CURRENT", 0, kCurrentFile},
{"LOCK", 0, kDBLockFile},
{"MANIFEST-2", 2, kDescriptorFile},
{"MANIFEST-7", 7, kDescriptorFile},
{"LOG", 0, kInfoLogFile},
{"LOG.old", 0, kInfoLogFile},
{"18446744073709551615.log", 18446744073709551615ull, kLogFile},
};
for (int i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
std::string f = cases[i].fname;
ASSERT_TRUE(ParseFileName(f, &number, &type)) << f;
ASSERT_EQ(cases[i].type, type) << f;
ASSERT_EQ(cases[i].number, number) << f;
}
// Errors
static const char* errors[] = {"",
"foo",
"foo-dx-100.log",
".log",
"",
"manifest",
"CURREN",
"CURRENTX",
"MANIFES",
"MANIFEST",
"MANIFEST-",
"XMANIFEST-3",
"MANIFEST-3x",
"LOC",
"LOCKx",
"LO",
"LOGx",
"18446744073709551616.log",
"184467440737095516150.log",
"100",
"100.",
"100.lop"};
for (int i = 0; i < sizeof(errors) / sizeof(errors[0]); i++) {
std::string f = errors[i];
ASSERT_TRUE(!ParseFileName(f, &number, &type)) << f;
}
}
TEST(FileNameTest, Construction) {
uint64_t number;
FileType type;
std::string fname;
fname = CurrentFileName("foo");
ASSERT_EQ("foo/", std::string(fname.data(), 4));
ASSERT_TRUE(ParseFileName(fname.c_str() + 4, &number, &type));
ASSERT_EQ(0, number);
ASSERT_EQ(kCurrentFile, type);
fname = LockFileName("foo");
ASSERT_EQ("foo/", std::string(fname.data(), 4));
ASSERT_TRUE(ParseFileName(fname.c_str() + 4, &number, &type));
ASSERT_EQ(0, number);
ASSERT_EQ(kDBLockFile, type);
fname = LogFileName("foo", 192);
ASSERT_EQ("foo/", std::string(fname.data(), 4));
ASSERT_TRUE(ParseFileName(fname.c_str() + 4, &number, &type));
ASSERT_EQ(192, number);
ASSERT_EQ(kLogFile, type);
fname = TableFileName("bar", 200);
ASSERT_EQ("bar/", std::string(fname.data(), 4));
ASSERT_TRUE(ParseFileName(fname.c_str() + 4, &number, &type));
ASSERT_EQ(200, number);
ASSERT_EQ(kTableFile, type);
fname = DescriptorFileName("bar", 100);
ASSERT_EQ("bar/", std::string(fname.data(), 4));
ASSERT_TRUE(ParseFileName(fname.c_str() + 4, &number, &type));
ASSERT_EQ(100, number);
ASSERT_EQ(kDescriptorFile, type);
fname = TempFileName("tmp", 999);
ASSERT_EQ("tmp/", std::string(fname.data(), 4));
ASSERT_TRUE(ParseFileName(fname.c_str() + 4, &number, &type));
ASSERT_EQ(999, number);
ASSERT_EQ(kTempFile, type);
fname = InfoLogFileName("foo");
ASSERT_EQ("foo/", std::string(fname.data(), 4));
ASSERT_TRUE(ParseFileName(fname.c_str() + 4, &number, &type));
ASSERT_EQ(0, number);
ASSERT_EQ(kInfoLogFile, type);
fname = OldInfoLogFileName("foo");
ASSERT_EQ("foo/", std::string(fname.data(), 4));
ASSERT_TRUE(ParseFileName(fname.c_str() + 4, &number, &type));
ASSERT_EQ(0, number);
ASSERT_EQ(kInfoLogFile, type);
}
} // namespace leveldb
@@ -0,0 +1,64 @@
// Copyright (c) 2012 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <cstdio>
#include "leveldb/dumpfile.h"
#include "leveldb/env.h"
#include "leveldb/status.h"
namespace leveldb {
namespace {
class StdoutPrinter : public WritableFile {
public:
Status Append(const Slice& data) override {
fwrite(data.data(), 1, data.size(), stdout);
return Status::OK();
}
Status Close() override { return Status::OK(); }
Status Flush() override { return Status::OK(); }
Status Sync() override { return Status::OK(); }
};
bool HandleDumpCommand(Env* env, char** files, int num) {
StdoutPrinter printer;
bool ok = true;
for (int i = 0; i < num; i++) {
Status s = DumpFile(env, files[i], &printer);
if (!s.ok()) {
std::fprintf(stderr, "%s\n", s.ToString().c_str());
ok = false;
}
}
return ok;
}
} // namespace
} // namespace leveldb
static void Usage() {
std::fprintf(
stderr,
"Usage: leveldbutil command...\n"
" dump files... -- dump contents of specified files\n");
}
int main(int argc, char** argv) {
leveldb::Env* env = leveldb::Env::Default();
bool ok = true;
if (argc < 2) {
Usage();
ok = false;
} else {
std::string command = argv[1];
if (command == "dump") {
ok = leveldb::HandleDumpCommand(env, argv + 2, argc - 2);
} else {
Usage();
ok = false;
}
}
return (ok ? 0 : 1);
}
@@ -0,0 +1,35 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// Log format information shared by reader and writer.
// See ../doc/log_format.md for more detail.
#ifndef STORAGE_LEVELDB_DB_LOG_FORMAT_H_
#define STORAGE_LEVELDB_DB_LOG_FORMAT_H_
namespace leveldb {
namespace log {
enum RecordType {
// Zero is reserved for preallocated files
kZeroType = 0,
kFullType = 1,
// For fragments
kFirstType = 2,
kMiddleType = 3,
kLastType = 4
};
static const int kMaxRecordType = kLastType;
static const int kBlockSize = 32768;
// Header is checksum (4 bytes), length (2 bytes), type (1 byte).
static const int kHeaderSize = 4 + 2 + 1;
} // namespace log
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_LOG_FORMAT_H_
@@ -0,0 +1,274 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/log_reader.h"
#include <cstdio>
#include "leveldb/env.h"
#include "util/coding.h"
#include "util/crc32c.h"
namespace leveldb {
namespace log {
Reader::Reporter::~Reporter() = default;
Reader::Reader(SequentialFile* file, Reporter* reporter, bool checksum,
uint64_t initial_offset)
: file_(file),
reporter_(reporter),
checksum_(checksum),
backing_store_(new char[kBlockSize]),
buffer_(),
eof_(false),
last_record_offset_(0),
end_of_buffer_offset_(0),
initial_offset_(initial_offset),
resyncing_(initial_offset > 0) {}
Reader::~Reader() { delete[] backing_store_; }
bool Reader::SkipToInitialBlock() {
const size_t offset_in_block = initial_offset_ % kBlockSize;
uint64_t block_start_location = initial_offset_ - offset_in_block;
// Don't search a block if we'd be in the trailer
if (offset_in_block > kBlockSize - 6) {
block_start_location += kBlockSize;
}
end_of_buffer_offset_ = block_start_location;
// Skip to start of first block that can contain the initial record
if (block_start_location > 0) {
Status skip_status = file_->Skip(block_start_location);
if (!skip_status.ok()) {
ReportDrop(block_start_location, skip_status);
return false;
}
}
return true;
}
bool Reader::ReadRecord(Slice* record, std::string* scratch) {
if (last_record_offset_ < initial_offset_) {
if (!SkipToInitialBlock()) {
return false;
}
}
scratch->clear();
record->clear();
bool in_fragmented_record = false;
// Record offset of the logical record that we're reading
// 0 is a dummy value to make compilers happy
uint64_t prospective_record_offset = 0;
Slice fragment;
while (true) {
const unsigned int record_type = ReadPhysicalRecord(&fragment);
// ReadPhysicalRecord may have only had an empty trailer remaining in its
// internal buffer. Calculate the offset of the next physical record now
// that it has returned, properly accounting for its header size.
uint64_t physical_record_offset =
end_of_buffer_offset_ - buffer_.size() - kHeaderSize - fragment.size();
if (resyncing_) {
if (record_type == kMiddleType) {
continue;
} else if (record_type == kLastType) {
resyncing_ = false;
continue;
} else {
resyncing_ = false;
}
}
switch (record_type) {
case kFullType:
if (in_fragmented_record) {
// Handle bug in earlier versions of log::Writer where
// it could emit an empty kFirstType record at the tail end
// of a block followed by a kFullType or kFirstType record
// at the beginning of the next block.
if (!scratch->empty()) {
ReportCorruption(scratch->size(), "partial record without end(1)");
}
}
prospective_record_offset = physical_record_offset;
scratch->clear();
*record = fragment;
last_record_offset_ = prospective_record_offset;
return true;
case kFirstType:
if (in_fragmented_record) {
// Handle bug in earlier versions of log::Writer where
// it could emit an empty kFirstType record at the tail end
// of a block followed by a kFullType or kFirstType record
// at the beginning of the next block.
if (!scratch->empty()) {
ReportCorruption(scratch->size(), "partial record without end(2)");
}
}
prospective_record_offset = physical_record_offset;
scratch->assign(fragment.data(), fragment.size());
in_fragmented_record = true;
break;
case kMiddleType:
if (!in_fragmented_record) {
ReportCorruption(fragment.size(),
"missing start of fragmented record(1)");
} else {
scratch->append(fragment.data(), fragment.size());
}
break;
case kLastType:
if (!in_fragmented_record) {
ReportCorruption(fragment.size(),
"missing start of fragmented record(2)");
} else {
scratch->append(fragment.data(), fragment.size());
*record = Slice(*scratch);
last_record_offset_ = prospective_record_offset;
return true;
}
break;
case kEof:
if (in_fragmented_record) {
// This can be caused by the writer dying immediately after
// writing a physical record but before completing the next; don't
// treat it as a corruption, just ignore the entire logical record.
scratch->clear();
}
return false;
case kBadRecord:
if (in_fragmented_record) {
ReportCorruption(scratch->size(), "error in middle of record");
in_fragmented_record = false;
scratch->clear();
}
break;
default: {
char buf[40];
std::snprintf(buf, sizeof(buf), "unknown record type %u", record_type);
ReportCorruption(
(fragment.size() + (in_fragmented_record ? scratch->size() : 0)),
buf);
in_fragmented_record = false;
scratch->clear();
break;
}
}
}
return false;
}
uint64_t Reader::LastRecordOffset() { return last_record_offset_; }
void Reader::ReportCorruption(uint64_t bytes, const char* reason) {
ReportDrop(bytes, Status::Corruption(reason));
}
void Reader::ReportDrop(uint64_t bytes, const Status& reason) {
if (reporter_ != nullptr &&
end_of_buffer_offset_ - buffer_.size() - bytes >= initial_offset_) {
reporter_->Corruption(static_cast<size_t>(bytes), reason);
}
}
unsigned int Reader::ReadPhysicalRecord(Slice* result) {
while (true) {
if (buffer_.size() < kHeaderSize) {
if (!eof_) {
// Last read was a full read, so this is a trailer to skip
buffer_.clear();
Status status = file_->Read(kBlockSize, &buffer_, backing_store_);
end_of_buffer_offset_ += buffer_.size();
if (!status.ok()) {
buffer_.clear();
ReportDrop(kBlockSize, status);
eof_ = true;
return kEof;
} else if (buffer_.size() < kBlockSize) {
eof_ = true;
}
continue;
} else {
// Note that if buffer_ is non-empty, we have a truncated header at the
// end of the file, which can be caused by the writer crashing in the
// middle of writing the header. Instead of considering this an error,
// just report EOF.
buffer_.clear();
return kEof;
}
}
// Parse the header
const char* header = buffer_.data();
const uint32_t a = static_cast<uint32_t>(header[4]) & 0xff;
const uint32_t b = static_cast<uint32_t>(header[5]) & 0xff;
const unsigned int type = header[6];
const uint32_t length = a | (b << 8);
if (kHeaderSize + length > buffer_.size()) {
size_t drop_size = buffer_.size();
buffer_.clear();
if (!eof_) {
ReportCorruption(drop_size, "bad record length");
return kBadRecord;
}
// If the end of the file has been reached without reading |length| bytes
// of payload, assume the writer died in the middle of writing the record.
// Don't report a corruption.
return kEof;
}
if (type == kZeroType && length == 0) {
// Skip zero length record without reporting any drops since
// such records are produced by the mmap based writing code in
// env_posix.cc that preallocates file regions.
buffer_.clear();
return kBadRecord;
}
// Check crc
if (checksum_) {
uint32_t expected_crc = crc32c::Unmask(DecodeFixed32(header));
uint32_t actual_crc = crc32c::Value(header + 6, 1 + length);
if (actual_crc != expected_crc) {
// Drop the rest of the buffer since "length" itself may have
// been corrupted and if we trust it, we could find some
// fragment of a real log record that just happens to look
// like a valid log record.
size_t drop_size = buffer_.size();
buffer_.clear();
ReportCorruption(drop_size, "checksum mismatch");
return kBadRecord;
}
}
buffer_.remove_prefix(kHeaderSize + length);
// Skip physical record that started before initial_offset_
if (end_of_buffer_offset_ - buffer_.size() - kHeaderSize - length <
initial_offset_) {
result->clear();
return kBadRecord;
}
*result = Slice(header + kHeaderSize, length);
return type;
}
}
} // namespace log
} // namespace leveldb
@@ -0,0 +1,112 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_DB_LOG_READER_H_
#define STORAGE_LEVELDB_DB_LOG_READER_H_
#include <cstdint>
#include "db/log_format.h"
#include "leveldb/slice.h"
#include "leveldb/status.h"
namespace leveldb {
class SequentialFile;
namespace log {
class Reader {
public:
// Interface for reporting errors.
class Reporter {
public:
virtual ~Reporter();
// Some corruption was detected. "bytes" is the approximate number
// of bytes dropped due to the corruption.
virtual void Corruption(size_t bytes, const Status& status) = 0;
};
// Create a reader that will return log records from "*file".
// "*file" must remain live while this Reader is in use.
//
// If "reporter" is non-null, it is notified whenever some data is
// dropped due to a detected corruption. "*reporter" must remain
// live while this Reader is in use.
//
// If "checksum" is true, verify checksums if available.
//
// The Reader will start reading at the first record located at physical
// position >= initial_offset within the file.
Reader(SequentialFile* file, Reporter* reporter, bool checksum,
uint64_t initial_offset);
Reader(const Reader&) = delete;
Reader& operator=(const Reader&) = delete;
~Reader();
// Read the next record into *record. Returns true if read
// successfully, false if we hit end of the input. May use
// "*scratch" as temporary storage. The contents filled in *record
// will only be valid until the next mutating operation on this
// reader or the next mutation to *scratch.
bool ReadRecord(Slice* record, std::string* scratch);
// Returns the physical offset of the last record returned by ReadRecord.
//
// Undefined before the first call to ReadRecord.
uint64_t LastRecordOffset();
private:
// Extend record types with the following special values
enum {
kEof = kMaxRecordType + 1,
// Returned whenever we find an invalid physical record.
// Currently there are three situations in which this happens:
// * The record has an invalid CRC (ReadPhysicalRecord reports a drop)
// * The record is a 0-length record (No drop is reported)
// * The record is below constructor's initial_offset (No drop is reported)
kBadRecord = kMaxRecordType + 2
};
// Skips all blocks that are completely before "initial_offset_".
//
// Returns true on success. Handles reporting.
bool SkipToInitialBlock();
// Return type, or one of the preceding special values
unsigned int ReadPhysicalRecord(Slice* result);
// Reports dropped bytes to the reporter.
// buffer_ must be updated to remove the dropped bytes prior to invocation.
void ReportCorruption(uint64_t bytes, const char* reason);
void ReportDrop(uint64_t bytes, const Status& reason);
SequentialFile* const file_;
Reporter* const reporter_;
bool const checksum_;
char* const backing_store_;
Slice buffer_;
bool eof_; // Last Read() indicated EOF by returning < kBlockSize
// Offset of the last record returned by ReadRecord.
uint64_t last_record_offset_;
// Offset of the first location past the end of buffer_.
uint64_t end_of_buffer_offset_;
// Offset at which to start looking for the first record to return
uint64_t const initial_offset_;
// True if we are resynchronizing after a seek (initial_offset_ > 0). In
// particular, a run of kMiddleType and kLastType records can be silently
// skipped in this mode
bool resyncing_;
};
} // namespace log
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_LOG_READER_H_
@@ -0,0 +1,558 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "gtest/gtest.h"
#include "db/log_reader.h"
#include "db/log_writer.h"
#include "leveldb/env.h"
#include "util/coding.h"
#include "util/crc32c.h"
#include "util/random.h"
namespace leveldb {
namespace log {
// Construct a string of the specified length made out of the supplied
// partial string.
static std::string BigString(const std::string& partial_string, size_t n) {
std::string result;
while (result.size() < n) {
result.append(partial_string);
}
result.resize(n);
return result;
}
// Construct a string from a number
static std::string NumberString(int n) {
char buf[50];
std::snprintf(buf, sizeof(buf), "%d.", n);
return std::string(buf);
}
// Return a skewed potentially long string
static std::string RandomSkewedString(int i, Random* rnd) {
return BigString(NumberString(i), rnd->Skewed(17));
}
class LogTest : public testing::Test {
public:
LogTest()
: reading_(false),
writer_(new Writer(&dest_)),
reader_(new Reader(&source_, &report_, true /*checksum*/,
0 /*initial_offset*/)) {}
~LogTest() {
delete writer_;
delete reader_;
}
void ReopenForAppend() {
delete writer_;
writer_ = new Writer(&dest_, dest_.contents_.size());
}
void Write(const std::string& msg) {
ASSERT_TRUE(!reading_) << "Write() after starting to read";
writer_->AddRecord(Slice(msg));
}
size_t WrittenBytes() const { return dest_.contents_.size(); }
std::string Read() {
if (!reading_) {
reading_ = true;
source_.contents_ = Slice(dest_.contents_);
}
std::string scratch;
Slice record;
if (reader_->ReadRecord(&record, &scratch)) {
return record.ToString();
} else {
return "EOF";
}
}
void IncrementByte(int offset, int delta) {
dest_.contents_[offset] += delta;
}
void SetByte(int offset, char new_byte) {
dest_.contents_[offset] = new_byte;
}
void ShrinkSize(int bytes) {
dest_.contents_.resize(dest_.contents_.size() - bytes);
}
void FixChecksum(int header_offset, int len) {
// Compute crc of type/len/data
uint32_t crc = crc32c::Value(&dest_.contents_[header_offset + 6], 1 + len);
crc = crc32c::Mask(crc);
EncodeFixed32(&dest_.contents_[header_offset], crc);
}
void ForceError() { source_.force_error_ = true; }
size_t DroppedBytes() const { return report_.dropped_bytes_; }
std::string ReportMessage() const { return report_.message_; }
// Returns OK iff recorded error message contains "msg"
std::string MatchError(const std::string& msg) const {
if (report_.message_.find(msg) == std::string::npos) {
return report_.message_;
} else {
return "OK";
}
}
void WriteInitialOffsetLog() {
for (int i = 0; i < num_initial_offset_records_; i++) {
std::string record(initial_offset_record_sizes_[i],
static_cast<char>('a' + i));
Write(record);
}
}
void StartReadingAt(uint64_t initial_offset) {
delete reader_;
reader_ = new Reader(&source_, &report_, true /*checksum*/, initial_offset);
}
void CheckOffsetPastEndReturnsNoRecords(uint64_t offset_past_end) {
WriteInitialOffsetLog();
reading_ = true;
source_.contents_ = Slice(dest_.contents_);
Reader* offset_reader = new Reader(&source_, &report_, true /*checksum*/,
WrittenBytes() + offset_past_end);
Slice record;
std::string scratch;
ASSERT_TRUE(!offset_reader->ReadRecord(&record, &scratch));
delete offset_reader;
}
void CheckInitialOffsetRecord(uint64_t initial_offset,
int expected_record_offset) {
WriteInitialOffsetLog();
reading_ = true;
source_.contents_ = Slice(dest_.contents_);
Reader* offset_reader =
new Reader(&source_, &report_, true /*checksum*/, initial_offset);
// Read all records from expected_record_offset through the last one.
ASSERT_LT(expected_record_offset, num_initial_offset_records_);
for (; expected_record_offset < num_initial_offset_records_;
++expected_record_offset) {
Slice record;
std::string scratch;
ASSERT_TRUE(offset_reader->ReadRecord(&record, &scratch));
ASSERT_EQ(initial_offset_record_sizes_[expected_record_offset],
record.size());
ASSERT_EQ(initial_offset_last_record_offsets_[expected_record_offset],
offset_reader->LastRecordOffset());
ASSERT_EQ((char)('a' + expected_record_offset), record.data()[0]);
}
delete offset_reader;
}
private:
class StringDest : public WritableFile {
public:
Status Close() override { return Status::OK(); }
Status Flush() override { return Status::OK(); }
Status Sync() override { return Status::OK(); }
Status Append(const Slice& slice) override {
contents_.append(slice.data(), slice.size());
return Status::OK();
}
std::string contents_;
};
class StringSource : public SequentialFile {
public:
StringSource() : force_error_(false), returned_partial_(false) {}
Status Read(size_t n, Slice* result, char* scratch) override {
EXPECT_TRUE(!returned_partial_) << "must not Read() after eof/error";
if (force_error_) {
force_error_ = false;
returned_partial_ = true;
return Status::Corruption("read error");
}
if (contents_.size() < n) {
n = contents_.size();
returned_partial_ = true;
}
*result = Slice(contents_.data(), n);
contents_.remove_prefix(n);
return Status::OK();
}
Status Skip(uint64_t n) override {
if (n > contents_.size()) {
contents_.clear();
return Status::NotFound("in-memory file skipped past end");
}
contents_.remove_prefix(n);
return Status::OK();
}
Slice contents_;
bool force_error_;
bool returned_partial_;
};
class ReportCollector : public Reader::Reporter {
public:
ReportCollector() : dropped_bytes_(0) {}
void Corruption(size_t bytes, const Status& status) override {
dropped_bytes_ += bytes;
message_.append(status.ToString());
}
size_t dropped_bytes_;
std::string message_;
};
// Record metadata for testing initial offset functionality
static size_t initial_offset_record_sizes_[];
static uint64_t initial_offset_last_record_offsets_[];
static int num_initial_offset_records_;
StringDest dest_;
StringSource source_;
ReportCollector report_;
bool reading_;
Writer* writer_;
Reader* reader_;
};
size_t LogTest::initial_offset_record_sizes_[] = {
10000, // Two sizable records in first block
10000,
2 * log::kBlockSize - 1000, // Span three blocks
1,
13716, // Consume all but two bytes of block 3.
log::kBlockSize - kHeaderSize, // Consume the entirety of block 4.
};
uint64_t LogTest::initial_offset_last_record_offsets_[] = {
0,
kHeaderSize + 10000,
2 * (kHeaderSize + 10000),
2 * (kHeaderSize + 10000) + (2 * log::kBlockSize - 1000) + 3 * kHeaderSize,
2 * (kHeaderSize + 10000) + (2 * log::kBlockSize - 1000) + 3 * kHeaderSize +
kHeaderSize + 1,
3 * log::kBlockSize,
};
// LogTest::initial_offset_last_record_offsets_ must be defined before this.
int LogTest::num_initial_offset_records_ =
sizeof(LogTest::initial_offset_last_record_offsets_) / sizeof(uint64_t);
TEST_F(LogTest, Empty) { ASSERT_EQ("EOF", Read()); }
TEST_F(LogTest, ReadWrite) {
Write("foo");
Write("bar");
Write("");
Write("xxxx");
ASSERT_EQ("foo", Read());
ASSERT_EQ("bar", Read());
ASSERT_EQ("", Read());
ASSERT_EQ("xxxx", Read());
ASSERT_EQ("EOF", Read());
ASSERT_EQ("EOF", Read()); // Make sure reads at eof work
}
TEST_F(LogTest, ManyBlocks) {
for (int i = 0; i < 100000; i++) {
Write(NumberString(i));
}
for (int i = 0; i < 100000; i++) {
ASSERT_EQ(NumberString(i), Read());
}
ASSERT_EQ("EOF", Read());
}
TEST_F(LogTest, Fragmentation) {
Write("small");
Write(BigString("medium", 50000));
Write(BigString("large", 100000));
ASSERT_EQ("small", Read());
ASSERT_EQ(BigString("medium", 50000), Read());
ASSERT_EQ(BigString("large", 100000), Read());
ASSERT_EQ("EOF", Read());
}
TEST_F(LogTest, MarginalTrailer) {
// Make a trailer that is exactly the same length as an empty record.
const int n = kBlockSize - 2 * kHeaderSize;
Write(BigString("foo", n));
ASSERT_EQ(kBlockSize - kHeaderSize, WrittenBytes());
Write("");
Write("bar");
ASSERT_EQ(BigString("foo", n), Read());
ASSERT_EQ("", Read());
ASSERT_EQ("bar", Read());
ASSERT_EQ("EOF", Read());
}
TEST_F(LogTest, MarginalTrailer2) {
// Make a trailer that is exactly the same length as an empty record.
const int n = kBlockSize - 2 * kHeaderSize;
Write(BigString("foo", n));
ASSERT_EQ(kBlockSize - kHeaderSize, WrittenBytes());
Write("bar");
ASSERT_EQ(BigString("foo", n), Read());
ASSERT_EQ("bar", Read());
ASSERT_EQ("EOF", Read());
ASSERT_EQ(0, DroppedBytes());
ASSERT_EQ("", ReportMessage());
}
TEST_F(LogTest, ShortTrailer) {
const int n = kBlockSize - 2 * kHeaderSize + 4;
Write(BigString("foo", n));
ASSERT_EQ(kBlockSize - kHeaderSize + 4, WrittenBytes());
Write("");
Write("bar");
ASSERT_EQ(BigString("foo", n), Read());
ASSERT_EQ("", Read());
ASSERT_EQ("bar", Read());
ASSERT_EQ("EOF", Read());
}
TEST_F(LogTest, AlignedEof) {
const int n = kBlockSize - 2 * kHeaderSize + 4;
Write(BigString("foo", n));
ASSERT_EQ(kBlockSize - kHeaderSize + 4, WrittenBytes());
ASSERT_EQ(BigString("foo", n), Read());
ASSERT_EQ("EOF", Read());
}
TEST_F(LogTest, OpenForAppend) {
Write("hello");
ReopenForAppend();
Write("world");
ASSERT_EQ("hello", Read());
ASSERT_EQ("world", Read());
ASSERT_EQ("EOF", Read());
}
TEST_F(LogTest, RandomRead) {
const int N = 500;
Random write_rnd(301);
for (int i = 0; i < N; i++) {
Write(RandomSkewedString(i, &write_rnd));
}
Random read_rnd(301);
for (int i = 0; i < N; i++) {
ASSERT_EQ(RandomSkewedString(i, &read_rnd), Read());
}
ASSERT_EQ("EOF", Read());
}
// Tests of all the error paths in log_reader.cc follow:
TEST_F(LogTest, ReadError) {
Write("foo");
ForceError();
ASSERT_EQ("EOF", Read());
ASSERT_EQ(kBlockSize, DroppedBytes());
ASSERT_EQ("OK", MatchError("read error"));
}
TEST_F(LogTest, BadRecordType) {
Write("foo");
// Type is stored in header[6]
IncrementByte(6, 100);
FixChecksum(0, 3);
ASSERT_EQ("EOF", Read());
ASSERT_EQ(3, DroppedBytes());
ASSERT_EQ("OK", MatchError("unknown record type"));
}
TEST_F(LogTest, TruncatedTrailingRecordIsIgnored) {
Write("foo");
ShrinkSize(4); // Drop all payload as well as a header byte
ASSERT_EQ("EOF", Read());
// Truncated last record is ignored, not treated as an error.
ASSERT_EQ(0, DroppedBytes());
ASSERT_EQ("", ReportMessage());
}
TEST_F(LogTest, BadLength) {
const int kPayloadSize = kBlockSize - kHeaderSize;
Write(BigString("bar", kPayloadSize));
Write("foo");
// Least significant size byte is stored in header[4].
IncrementByte(4, 1);
ASSERT_EQ("foo", Read());
ASSERT_EQ(kBlockSize, DroppedBytes());
ASSERT_EQ("OK", MatchError("bad record length"));
}
TEST_F(LogTest, BadLengthAtEndIsIgnored) {
Write("foo");
ShrinkSize(1);
ASSERT_EQ("EOF", Read());
ASSERT_EQ(0, DroppedBytes());
ASSERT_EQ("", ReportMessage());
}
TEST_F(LogTest, ChecksumMismatch) {
Write("foo");
IncrementByte(0, 10);
ASSERT_EQ("EOF", Read());
ASSERT_EQ(10, DroppedBytes());
ASSERT_EQ("OK", MatchError("checksum mismatch"));
}
TEST_F(LogTest, UnexpectedMiddleType) {
Write("foo");
SetByte(6, kMiddleType);
FixChecksum(0, 3);
ASSERT_EQ("EOF", Read());
ASSERT_EQ(3, DroppedBytes());
ASSERT_EQ("OK", MatchError("missing start"));
}
TEST_F(LogTest, UnexpectedLastType) {
Write("foo");
SetByte(6, kLastType);
FixChecksum(0, 3);
ASSERT_EQ("EOF", Read());
ASSERT_EQ(3, DroppedBytes());
ASSERT_EQ("OK", MatchError("missing start"));
}
TEST_F(LogTest, UnexpectedFullType) {
Write("foo");
Write("bar");
SetByte(6, kFirstType);
FixChecksum(0, 3);
ASSERT_EQ("bar", Read());
ASSERT_EQ("EOF", Read());
ASSERT_EQ(3, DroppedBytes());
ASSERT_EQ("OK", MatchError("partial record without end"));
}
TEST_F(LogTest, UnexpectedFirstType) {
Write("foo");
Write(BigString("bar", 100000));
SetByte(6, kFirstType);
FixChecksum(0, 3);
ASSERT_EQ(BigString("bar", 100000), Read());
ASSERT_EQ("EOF", Read());
ASSERT_EQ(3, DroppedBytes());
ASSERT_EQ("OK", MatchError("partial record without end"));
}
TEST_F(LogTest, MissingLastIsIgnored) {
Write(BigString("bar", kBlockSize));
// Remove the LAST block, including header.
ShrinkSize(14);
ASSERT_EQ("EOF", Read());
ASSERT_EQ("", ReportMessage());
ASSERT_EQ(0, DroppedBytes());
}
TEST_F(LogTest, PartialLastIsIgnored) {
Write(BigString("bar", kBlockSize));
// Cause a bad record length in the LAST block.
ShrinkSize(1);
ASSERT_EQ("EOF", Read());
ASSERT_EQ("", ReportMessage());
ASSERT_EQ(0, DroppedBytes());
}
TEST_F(LogTest, SkipIntoMultiRecord) {
// Consider a fragmented record:
// first(R1), middle(R1), last(R1), first(R2)
// If initial_offset points to a record after first(R1) but before first(R2)
// incomplete fragment errors are not actual errors, and must be suppressed
// until a new first or full record is encountered.
Write(BigString("foo", 3 * kBlockSize));
Write("correct");
StartReadingAt(kBlockSize);
ASSERT_EQ("correct", Read());
ASSERT_EQ("", ReportMessage());
ASSERT_EQ(0, DroppedBytes());
ASSERT_EQ("EOF", Read());
}
TEST_F(LogTest, ErrorJoinsRecords) {
// Consider two fragmented records:
// first(R1) last(R1) first(R2) last(R2)
// where the middle two fragments disappear. We do not want
// first(R1),last(R2) to get joined and returned as a valid record.
// Write records that span two blocks
Write(BigString("foo", kBlockSize));
Write(BigString("bar", kBlockSize));
Write("correct");
// Wipe the middle block
for (int offset = kBlockSize; offset < 2 * kBlockSize; offset++) {
SetByte(offset, 'x');
}
ASSERT_EQ("correct", Read());
ASSERT_EQ("EOF", Read());
const size_t dropped = DroppedBytes();
ASSERT_LE(dropped, 2 * kBlockSize + 100);
ASSERT_GE(dropped, 2 * kBlockSize);
}
TEST_F(LogTest, ReadStart) { CheckInitialOffsetRecord(0, 0); }
TEST_F(LogTest, ReadSecondOneOff) { CheckInitialOffsetRecord(1, 1); }
TEST_F(LogTest, ReadSecondTenThousand) { CheckInitialOffsetRecord(10000, 1); }
TEST_F(LogTest, ReadSecondStart) { CheckInitialOffsetRecord(10007, 1); }
TEST_F(LogTest, ReadThirdOneOff) { CheckInitialOffsetRecord(10008, 2); }
TEST_F(LogTest, ReadThirdStart) { CheckInitialOffsetRecord(20014, 2); }
TEST_F(LogTest, ReadFourthOneOff) { CheckInitialOffsetRecord(20015, 3); }
TEST_F(LogTest, ReadFourthFirstBlockTrailer) {
CheckInitialOffsetRecord(log::kBlockSize - 4, 3);
}
TEST_F(LogTest, ReadFourthMiddleBlock) {
CheckInitialOffsetRecord(log::kBlockSize + 1, 3);
}
TEST_F(LogTest, ReadFourthLastBlock) {
CheckInitialOffsetRecord(2 * log::kBlockSize + 1, 3);
}
TEST_F(LogTest, ReadFourthStart) {
CheckInitialOffsetRecord(
2 * (kHeaderSize + 1000) + (2 * log::kBlockSize - 1000) + 3 * kHeaderSize,
3);
}
TEST_F(LogTest, ReadInitialOffsetIntoBlockPadding) {
CheckInitialOffsetRecord(3 * log::kBlockSize - 3, 5);
}
TEST_F(LogTest, ReadEnd) { CheckOffsetPastEndReturnsNoRecords(0); }
TEST_F(LogTest, ReadPastEnd) { CheckOffsetPastEndReturnsNoRecords(5); }
} // namespace log
} // namespace leveldb
@@ -0,0 +1,111 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/log_writer.h"
#include <cstdint>
#include "leveldb/env.h"
#include "util/coding.h"
#include "util/crc32c.h"
namespace leveldb {
namespace log {
static void InitTypeCrc(uint32_t* type_crc) {
for (int i = 0; i <= kMaxRecordType; i++) {
char t = static_cast<char>(i);
type_crc[i] = crc32c::Value(&t, 1);
}
}
Writer::Writer(WritableFile* dest) : dest_(dest), block_offset_(0) {
InitTypeCrc(type_crc_);
}
Writer::Writer(WritableFile* dest, uint64_t dest_length)
: dest_(dest), block_offset_(dest_length % kBlockSize) {
InitTypeCrc(type_crc_);
}
Writer::~Writer() = default;
Status Writer::AddRecord(const Slice& slice) {
const char* ptr = slice.data();
size_t left = slice.size();
// Fragment the record if necessary and emit it. Note that if slice
// is empty, we still want to iterate once to emit a single
// zero-length record
Status s;
bool begin = true;
do {
const int leftover = kBlockSize - block_offset_;
assert(leftover >= 0);
if (leftover < kHeaderSize) {
// Switch to a new block
if (leftover > 0) {
// Fill the trailer (literal below relies on kHeaderSize being 7)
static_assert(kHeaderSize == 7, "");
dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));
}
block_offset_ = 0;
}
// Invariant: we never leave < kHeaderSize bytes in a block.
assert(kBlockSize - block_offset_ - kHeaderSize >= 0);
const size_t avail = kBlockSize - block_offset_ - kHeaderSize;
const size_t fragment_length = (left < avail) ? left : avail;
RecordType type;
const bool end = (left == fragment_length);
if (begin && end) {
type = kFullType;
} else if (begin) {
type = kFirstType;
} else if (end) {
type = kLastType;
} else {
type = kMiddleType;
}
s = EmitPhysicalRecord(type, ptr, fragment_length);
ptr += fragment_length;
left -= fragment_length;
begin = false;
} while (s.ok() && left > 0);
return s;
}
Status Writer::EmitPhysicalRecord(RecordType t, const char* ptr,
size_t length) {
assert(length <= 0xffff); // Must fit in two bytes
assert(block_offset_ + kHeaderSize + length <= kBlockSize);
// Format the header
char buf[kHeaderSize];
buf[4] = static_cast<char>(length & 0xff);
buf[5] = static_cast<char>(length >> 8);
buf[6] = static_cast<char>(t);
// Compute the crc of the record type and the payload.
uint32_t crc = crc32c::Extend(type_crc_[t], ptr, length);
crc = crc32c::Mask(crc); // Adjust for storage
EncodeFixed32(buf, crc);
// Write the header and the payload
Status s = dest_->Append(Slice(buf, kHeaderSize));
if (s.ok()) {
s = dest_->Append(Slice(ptr, length));
if (s.ok()) {
s = dest_->Flush();
}
}
block_offset_ += kHeaderSize + length;
return s;
}
} // namespace log
} // namespace leveldb
@@ -0,0 +1,54 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_DB_LOG_WRITER_H_
#define STORAGE_LEVELDB_DB_LOG_WRITER_H_
#include <cstdint>
#include "db/log_format.h"
#include "leveldb/slice.h"
#include "leveldb/status.h"
namespace leveldb {
class WritableFile;
namespace log {
class Writer {
public:
// Create a writer that will append data to "*dest".
// "*dest" must be initially empty.
// "*dest" must remain live while this Writer is in use.
explicit Writer(WritableFile* dest);
// Create a writer that will append data to "*dest".
// "*dest" must have initial length "dest_length".
// "*dest" must remain live while this Writer is in use.
Writer(WritableFile* dest, uint64_t dest_length);
Writer(const Writer&) = delete;
Writer& operator=(const Writer&) = delete;
~Writer();
Status AddRecord(const Slice& slice);
private:
Status EmitPhysicalRecord(RecordType type, const char* ptr, size_t length);
WritableFile* dest_;
int block_offset_; // Current offset in block
// crc32c values for all supported record types. These are
// pre-computed to reduce the overhead of computing the crc of the
// record type stored in the header.
uint32_t type_crc_[kMaxRecordType + 1];
};
} // namespace log
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_LOG_WRITER_H_
@@ -0,0 +1,138 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/memtable.h"
#include "db/dbformat.h"
#include "leveldb/comparator.h"
#include "leveldb/env.h"
#include "leveldb/iterator.h"
#include "util/coding.h"
namespace leveldb {
static Slice GetLengthPrefixedSlice(const char* data) {
uint32_t len;
const char* p = data;
p = GetVarint32Ptr(p, p + 5, &len); // +5: we assume "p" is not corrupted
return Slice(p, len);
}
MemTable::MemTable(const InternalKeyComparator& comparator)
: comparator_(comparator), refs_(0), table_(comparator_, &arena_) {}
MemTable::~MemTable() { assert(refs_ == 0); }
size_t MemTable::ApproximateMemoryUsage() { return arena_.MemoryUsage(); }
int MemTable::KeyComparator::operator()(const char* aptr,
const char* bptr) const {
// Internal keys are encoded as length-prefixed strings.
Slice a = GetLengthPrefixedSlice(aptr);
Slice b = GetLengthPrefixedSlice(bptr);
return comparator.Compare(a, b);
}
// Encode a suitable internal key target for "target" and return it.
// Uses *scratch as scratch space, and the returned pointer will point
// into this scratch space.
static const char* EncodeKey(std::string* scratch, const Slice& target) {
scratch->clear();
PutVarint32(scratch, target.size());
scratch->append(target.data(), target.size());
return scratch->data();
}
class MemTableIterator : public Iterator {
public:
explicit MemTableIterator(MemTable::Table* table) : iter_(table) {}
MemTableIterator(const MemTableIterator&) = delete;
MemTableIterator& operator=(const MemTableIterator&) = delete;
~MemTableIterator() override = default;
bool Valid() const override { return iter_.Valid(); }
void Seek(const Slice& k) override { iter_.Seek(EncodeKey(&tmp_, k)); }
void SeekToFirst() override { iter_.SeekToFirst(); }
void SeekToLast() override { iter_.SeekToLast(); }
void Next() override { iter_.Next(); }
void Prev() override { iter_.Prev(); }
Slice key() const override { return GetLengthPrefixedSlice(iter_.key()); }
Slice value() const override {
Slice key_slice = GetLengthPrefixedSlice(iter_.key());
return GetLengthPrefixedSlice(key_slice.data() + key_slice.size());
}
Status status() const override { return Status::OK(); }
private:
MemTable::Table::Iterator iter_;
std::string tmp_; // For passing to EncodeKey
};
Iterator* MemTable::NewIterator() { return new MemTableIterator(&table_); }
void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,
const Slice& value) {
// Format of an entry is concatenation of:
// key_size : varint32 of internal_key.size()
// key bytes : char[internal_key.size()]
// tag : uint64((sequence << 8) | type)
// value_size : varint32 of value.size()
// value bytes : char[value.size()]
size_t key_size = key.size();
size_t val_size = value.size();
size_t internal_key_size = key_size + 8;
const size_t encoded_len = VarintLength(internal_key_size) +
internal_key_size + VarintLength(val_size) +
val_size;
char* buf = arena_.Allocate(encoded_len);
char* p = EncodeVarint32(buf, internal_key_size);
std::memcpy(p, key.data(), key_size);
p += key_size;
EncodeFixed64(p, (s << 8) | type);
p += 8;
p = EncodeVarint32(p, val_size);
std::memcpy(p, value.data(), val_size);
assert(p + val_size == buf + encoded_len);
table_.Insert(buf);
}
bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) {
Slice memkey = key.memtable_key();
Table::Iterator iter(&table_);
iter.Seek(memkey.data());
if (iter.Valid()) {
// entry format is:
// klength varint32
// userkey char[klength]
// tag uint64
// vlength varint32
// value char[vlength]
// Check that it belongs to same user key. We do not check the
// sequence number since the Seek() call above should have skipped
// all entries with overly large sequence numbers.
const char* entry = iter.key();
uint32_t key_length;
const char* key_ptr = GetVarint32Ptr(entry, entry + 5, &key_length);
if (comparator_.comparator.user_comparator()->Compare(
Slice(key_ptr, key_length - 8), key.user_key()) == 0) {
// Correct user key
const uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);
switch (static_cast<ValueType>(tag & 0xff)) {
case kTypeValue: {
Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
value->assign(v.data(), v.size());
return true;
}
case kTypeDeletion:
*s = Status::NotFound(Slice());
return true;
}
}
}
return false;
}
} // namespace leveldb
@@ -0,0 +1,87 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_DB_MEMTABLE_H_
#define STORAGE_LEVELDB_DB_MEMTABLE_H_
#include <string>
#include "db/dbformat.h"
#include "db/skiplist.h"
#include "leveldb/db.h"
#include "util/arena.h"
namespace leveldb {
class InternalKeyComparator;
class MemTableIterator;
class MemTable {
public:
// MemTables are reference counted. The initial reference count
// is zero and the caller must call Ref() at least once.
explicit MemTable(const InternalKeyComparator& comparator);
MemTable(const MemTable&) = delete;
MemTable& operator=(const MemTable&) = delete;
// Increase reference count.
void Ref() { ++refs_; }
// Drop reference count. Delete if no more references exist.
void Unref() {
--refs_;
assert(refs_ >= 0);
if (refs_ <= 0) {
delete this;
}
}
// Returns an estimate of the number of bytes of data in use by this
// data structure. It is safe to call when MemTable is being modified.
size_t ApproximateMemoryUsage();
// Return an iterator that yields the contents of the memtable.
//
// The caller must ensure that the underlying MemTable remains live
// while the returned iterator is live. The keys returned by this
// iterator are internal keys encoded by AppendInternalKey in the
// db/format.{h,cc} module.
Iterator* NewIterator();
// Add an entry into memtable that maps key to value at the
// specified sequence number and with the specified type.
// Typically value will be empty if type==kTypeDeletion.
void Add(SequenceNumber seq, ValueType type, const Slice& key,
const Slice& value);
// If memtable contains a value for key, store it in *value and return true.
// If memtable contains a deletion for key, store a NotFound() error
// in *status and return true.
// Else, return false.
bool Get(const LookupKey& key, std::string* value, Status* s);
private:
friend class MemTableIterator;
friend class MemTableBackwardIterator;
struct KeyComparator {
const InternalKeyComparator comparator;
explicit KeyComparator(const InternalKeyComparator& c) : comparator(c) {}
int operator()(const char* a, const char* b) const;
};
typedef SkipList<const char*, KeyComparator> Table;
~MemTable(); // Private since only Unref() should be used to delete it
KeyComparator comparator_;
int refs_;
Arena arena_;
Table table_;
};
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_MEMTABLE_H_
@@ -0,0 +1,339 @@
// Copyright (c) 2014 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "gtest/gtest.h"
#include "db/db_impl.h"
#include "db/filename.h"
#include "db/version_set.h"
#include "db/write_batch_internal.h"
#include "leveldb/db.h"
#include "leveldb/env.h"
#include "leveldb/write_batch.h"
#include "util/logging.h"
#include "util/testutil.h"
namespace leveldb {
class RecoveryTest : public testing::Test {
public:
RecoveryTest() : env_(Env::Default()), db_(nullptr) {
dbname_ = testing::TempDir() + "recovery_test";
DestroyDB(dbname_, Options());
Open();
}
~RecoveryTest() {
Close();
DestroyDB(dbname_, Options());
}
DBImpl* dbfull() const { return reinterpret_cast<DBImpl*>(db_); }
Env* env() const { return env_; }
bool CanAppend() {
WritableFile* tmp;
Status s = env_->NewAppendableFile(CurrentFileName(dbname_), &tmp);
delete tmp;
if (s.IsNotSupportedError()) {
return false;
} else {
return true;
}
}
void Close() {
delete db_;
db_ = nullptr;
}
Status OpenWithStatus(Options* options = nullptr) {
Close();
Options opts;
if (options != nullptr) {
opts = *options;
} else {
opts.reuse_logs = true; // TODO(sanjay): test both ways
opts.create_if_missing = true;
}
if (opts.env == nullptr) {
opts.env = env_;
}
return DB::Open(opts, dbname_, &db_);
}
void Open(Options* options = nullptr) {
ASSERT_LEVELDB_OK(OpenWithStatus(options));
ASSERT_EQ(1, NumLogs());
}
Status Put(const std::string& k, const std::string& v) {
return db_->Put(WriteOptions(), k, v);
}
std::string Get(const std::string& k, const Snapshot* snapshot = nullptr) {
std::string result;
Status s = db_->Get(ReadOptions(), k, &result);
if (s.IsNotFound()) {
result = "NOT_FOUND";
} else if (!s.ok()) {
result = s.ToString();
}
return result;
}
std::string ManifestFileName() {
std::string current;
EXPECT_LEVELDB_OK(
ReadFileToString(env_, CurrentFileName(dbname_), &current));
size_t len = current.size();
if (len > 0 && current[len - 1] == '\n') {
current.resize(len - 1);
}
return dbname_ + "/" + current;
}
std::string LogName(uint64_t number) { return LogFileName(dbname_, number); }
size_t RemoveLogFiles() {
// Linux allows unlinking open files, but Windows does not.
// Closing the db allows for file deletion.
Close();
std::vector<uint64_t> logs = GetFiles(kLogFile);
for (size_t i = 0; i < logs.size(); i++) {
EXPECT_LEVELDB_OK(env_->RemoveFile(LogName(logs[i]))) << LogName(logs[i]);
}
return logs.size();
}
void RemoveManifestFile() {
ASSERT_LEVELDB_OK(env_->RemoveFile(ManifestFileName()));
}
uint64_t FirstLogFile() { return GetFiles(kLogFile)[0]; }
std::vector<uint64_t> GetFiles(FileType t) {
std::vector<std::string> filenames;
EXPECT_LEVELDB_OK(env_->GetChildren(dbname_, &filenames));
std::vector<uint64_t> result;
for (size_t i = 0; i < filenames.size(); i++) {
uint64_t number;
FileType type;
if (ParseFileName(filenames[i], &number, &type) && type == t) {
result.push_back(number);
}
}
return result;
}
int NumLogs() { return GetFiles(kLogFile).size(); }
int NumTables() { return GetFiles(kTableFile).size(); }
uint64_t FileSize(const std::string& fname) {
uint64_t result;
EXPECT_LEVELDB_OK(env_->GetFileSize(fname, &result)) << fname;
return result;
}
void CompactMemTable() { dbfull()->TEST_CompactMemTable(); }
// Directly construct a log file that sets key to val.
void MakeLogFile(uint64_t lognum, SequenceNumber seq, Slice key, Slice val) {
std::string fname = LogFileName(dbname_, lognum);
WritableFile* file;
ASSERT_LEVELDB_OK(env_->NewWritableFile(fname, &file));
log::Writer writer(file);
WriteBatch batch;
batch.Put(key, val);
WriteBatchInternal::SetSequence(&batch, seq);
ASSERT_LEVELDB_OK(writer.AddRecord(WriteBatchInternal::Contents(&batch)));
ASSERT_LEVELDB_OK(file->Flush());
delete file;
}
private:
std::string dbname_;
Env* env_;
DB* db_;
};
TEST_F(RecoveryTest, ManifestReused) {
if (!CanAppend()) {
std::fprintf(stderr,
"skipping test because env does not support appending\n");
return;
}
ASSERT_LEVELDB_OK(Put("foo", "bar"));
Close();
std::string old_manifest = ManifestFileName();
Open();
ASSERT_EQ(old_manifest, ManifestFileName());
ASSERT_EQ("bar", Get("foo"));
Open();
ASSERT_EQ(old_manifest, ManifestFileName());
ASSERT_EQ("bar", Get("foo"));
}
TEST_F(RecoveryTest, LargeManifestCompacted) {
if (!CanAppend()) {
std::fprintf(stderr,
"skipping test because env does not support appending\n");
return;
}
ASSERT_LEVELDB_OK(Put("foo", "bar"));
Close();
std::string old_manifest = ManifestFileName();
// Pad with zeroes to make manifest file very big.
{
uint64_t len = FileSize(old_manifest);
WritableFile* file;
ASSERT_LEVELDB_OK(env()->NewAppendableFile(old_manifest, &file));
std::string zeroes(3 * 1048576 - static_cast<size_t>(len), 0);
ASSERT_LEVELDB_OK(file->Append(zeroes));
ASSERT_LEVELDB_OK(file->Flush());
delete file;
}
Open();
std::string new_manifest = ManifestFileName();
ASSERT_NE(old_manifest, new_manifest);
ASSERT_GT(10000, FileSize(new_manifest));
ASSERT_EQ("bar", Get("foo"));
Open();
ASSERT_EQ(new_manifest, ManifestFileName());
ASSERT_EQ("bar", Get("foo"));
}
TEST_F(RecoveryTest, NoLogFiles) {
ASSERT_LEVELDB_OK(Put("foo", "bar"));
ASSERT_EQ(1, RemoveLogFiles());
Open();
ASSERT_EQ("NOT_FOUND", Get("foo"));
Open();
ASSERT_EQ("NOT_FOUND", Get("foo"));
}
TEST_F(RecoveryTest, LogFileReuse) {
if (!CanAppend()) {
std::fprintf(stderr,
"skipping test because env does not support appending\n");
return;
}
for (int i = 0; i < 2; i++) {
ASSERT_LEVELDB_OK(Put("foo", "bar"));
if (i == 0) {
// Compact to ensure current log is empty
CompactMemTable();
}
Close();
ASSERT_EQ(1, NumLogs());
uint64_t number = FirstLogFile();
if (i == 0) {
ASSERT_EQ(0, FileSize(LogName(number)));
} else {
ASSERT_LT(0, FileSize(LogName(number)));
}
Open();
ASSERT_EQ(1, NumLogs());
ASSERT_EQ(number, FirstLogFile()) << "did not reuse log file";
ASSERT_EQ("bar", Get("foo"));
Open();
ASSERT_EQ(1, NumLogs());
ASSERT_EQ(number, FirstLogFile()) << "did not reuse log file";
ASSERT_EQ("bar", Get("foo"));
}
}
TEST_F(RecoveryTest, MultipleMemTables) {
// Make a large log.
const int kNum = 1000;
for (int i = 0; i < kNum; i++) {
char buf[100];
std::snprintf(buf, sizeof(buf), "%050d", i);
ASSERT_LEVELDB_OK(Put(buf, buf));
}
ASSERT_EQ(0, NumTables());
Close();
ASSERT_EQ(0, NumTables());
ASSERT_EQ(1, NumLogs());
uint64_t old_log_file = FirstLogFile();
// Force creation of multiple memtables by reducing the write buffer size.
Options opt;
opt.reuse_logs = true;
opt.write_buffer_size = (kNum * 100) / 2;
Open(&opt);
ASSERT_LE(2, NumTables());
ASSERT_EQ(1, NumLogs());
ASSERT_NE(old_log_file, FirstLogFile()) << "must not reuse log";
for (int i = 0; i < kNum; i++) {
char buf[100];
std::snprintf(buf, sizeof(buf), "%050d", i);
ASSERT_EQ(buf, Get(buf));
}
}
TEST_F(RecoveryTest, MultipleLogFiles) {
ASSERT_LEVELDB_OK(Put("foo", "bar"));
Close();
ASSERT_EQ(1, NumLogs());
// Make a bunch of uncompacted log files.
uint64_t old_log = FirstLogFile();
MakeLogFile(old_log + 1, 1000, "hello", "world");
MakeLogFile(old_log + 2, 1001, "hi", "there");
MakeLogFile(old_log + 3, 1002, "foo", "bar2");
// Recover and check that all log files were processed.
Open();
ASSERT_LE(1, NumTables());
ASSERT_EQ(1, NumLogs());
uint64_t new_log = FirstLogFile();
ASSERT_LE(old_log + 3, new_log);
ASSERT_EQ("bar2", Get("foo"));
ASSERT_EQ("world", Get("hello"));
ASSERT_EQ("there", Get("hi"));
// Test that previous recovery produced recoverable state.
Open();
ASSERT_LE(1, NumTables());
ASSERT_EQ(1, NumLogs());
if (CanAppend()) {
ASSERT_EQ(new_log, FirstLogFile());
}
ASSERT_EQ("bar2", Get("foo"));
ASSERT_EQ("world", Get("hello"));
ASSERT_EQ("there", Get("hi"));
// Check that introducing an older log file does not cause it to be re-read.
Close();
MakeLogFile(old_log + 1, 2000, "hello", "stale write");
Open();
ASSERT_LE(1, NumTables());
ASSERT_EQ(1, NumLogs());
if (CanAppend()) {
ASSERT_EQ(new_log, FirstLogFile());
}
ASSERT_EQ("bar2", Get("foo"));
ASSERT_EQ("world", Get("hello"));
ASSERT_EQ("there", Get("hi"));
}
TEST_F(RecoveryTest, ManifestMissing) {
ASSERT_LEVELDB_OK(Put("foo", "bar"));
Close();
RemoveManifestFile();
Status status = OpenWithStatus();
#if defined(LEVELDB_PLATFORM_CHROMIUM)
// TODO(crbug.com/760362): See comment in MakeIOError() from env_chromium.cc.
ASSERT_TRUE(status.IsIOError());
#else
ASSERT_TRUE(status.IsCorruption());
#endif // defined(LEVELDB_PLATFORM_CHROMIUM)
}
} // namespace leveldb
+451
View File
@@ -0,0 +1,451 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// We recover the contents of the descriptor from the other files we find.
// (1) Any log files are first converted to tables
// (2) We scan every table to compute
// (a) smallest/largest for the table
// (b) largest sequence number in the table
// (3) We generate descriptor contents:
// - log number is set to zero
// - next-file-number is set to 1 + largest file number we found
// - last-sequence-number is set to largest sequence# found across
// all tables (see 2c)
// - compaction pointers are cleared
// - every table file is added at level 0
//
// Possible optimization 1:
// (a) Compute total size and use to pick appropriate max-level M
// (b) Sort tables by largest sequence# in the table
// (c) For each table: if it overlaps earlier table, place in level-0,
// else place in level-M.
// Possible optimization 2:
// Store per-table metadata (smallest, largest, largest-seq#, ...)
// in the table's meta section to speed up ScanTable.
#include "db/builder.h"
#include "db/db_impl.h"
#include "db/dbformat.h"
#include "db/filename.h"
#include "db/log_reader.h"
#include "db/log_writer.h"
#include "db/memtable.h"
#include "db/table_cache.h"
#include "db/version_edit.h"
#include "db/write_batch_internal.h"
#include "leveldb/comparator.h"
#include "leveldb/db.h"
#include "leveldb/env.h"
namespace leveldb {
namespace {
class Repairer {
public:
Repairer(const std::string& dbname, const Options& options)
: dbname_(dbname),
env_(options.env),
icmp_(options.comparator),
ipolicy_(options.filter_policy),
options_(SanitizeOptions(dbname, &icmp_, &ipolicy_, options)),
owns_info_log_(options_.info_log != options.info_log),
owns_cache_(options_.block_cache != options.block_cache),
next_file_number_(1) {
// TableCache can be small since we expect each table to be opened once.
table_cache_ = new TableCache(dbname_, options_, 10);
}
~Repairer() {
delete table_cache_;
if (owns_info_log_) {
delete options_.info_log;
}
if (owns_cache_) {
delete options_.block_cache;
}
}
Status Run() {
Status status = FindFiles();
if (status.ok()) {
ConvertLogFilesToTables();
ExtractMetaData();
status = WriteDescriptor();
}
if (status.ok()) {
unsigned long long bytes = 0;
for (size_t i = 0; i < tables_.size(); i++) {
bytes += tables_[i].meta.file_size;
}
Log(options_.info_log,
"**** Repaired leveldb %s; "
"recovered %d files; %llu bytes. "
"Some data may have been lost. "
"****",
dbname_.c_str(), static_cast<int>(tables_.size()), bytes);
}
return status;
}
private:
struct TableInfo {
FileMetaData meta;
SequenceNumber max_sequence;
};
Status FindFiles() {
std::vector<std::string> filenames;
Status status = env_->GetChildren(dbname_, &filenames);
if (!status.ok()) {
return status;
}
if (filenames.empty()) {
return Status::IOError(dbname_, "repair found no files");
}
uint64_t number;
FileType type;
for (size_t i = 0; i < filenames.size(); i++) {
if (ParseFileName(filenames[i], &number, &type)) {
if (type == kDescriptorFile) {
manifests_.push_back(filenames[i]);
} else {
if (number + 1 > next_file_number_) {
next_file_number_ = number + 1;
}
if (type == kLogFile) {
logs_.push_back(number);
} else if (type == kTableFile) {
table_numbers_.push_back(number);
} else {
// Ignore other files
}
}
}
}
return status;
}
void ConvertLogFilesToTables() {
for (size_t i = 0; i < logs_.size(); i++) {
std::string logname = LogFileName(dbname_, logs_[i]);
Status status = ConvertLogToTable(logs_[i]);
if (!status.ok()) {
Log(options_.info_log, "Log #%llu: ignoring conversion error: %s",
(unsigned long long)logs_[i], status.ToString().c_str());
}
ArchiveFile(logname);
}
}
Status ConvertLogToTable(uint64_t log) {
struct LogReporter : public log::Reader::Reporter {
Env* env;
Logger* info_log;
uint64_t lognum;
void Corruption(size_t bytes, const Status& s) override {
// We print error messages for corruption, but continue repairing.
Log(info_log, "Log #%llu: dropping %d bytes; %s",
(unsigned long long)lognum, static_cast<int>(bytes),
s.ToString().c_str());
}
};
// Open the log file
std::string logname = LogFileName(dbname_, log);
SequentialFile* lfile;
Status status = env_->NewSequentialFile(logname, &lfile);
if (!status.ok()) {
return status;
}
// Create the log reader.
LogReporter reporter;
reporter.env = env_;
reporter.info_log = options_.info_log;
reporter.lognum = log;
// We intentionally make log::Reader do checksumming so that
// corruptions cause entire commits to be skipped instead of
// propagating bad information (like overly large sequence
// numbers).
log::Reader reader(lfile, &reporter, false /*do not checksum*/,
0 /*initial_offset*/);
// Read all the records and add to a memtable
std::string scratch;
Slice record;
WriteBatch batch;
MemTable* mem = new MemTable(icmp_);
mem->Ref();
int counter = 0;
while (reader.ReadRecord(&record, &scratch)) {
if (record.size() < 12) {
reporter.Corruption(record.size(),
Status::Corruption("log record too small"));
continue;
}
WriteBatchInternal::SetContents(&batch, record);
status = WriteBatchInternal::InsertInto(&batch, mem);
if (status.ok()) {
counter += WriteBatchInternal::Count(&batch);
} else {
Log(options_.info_log, "Log #%llu: ignoring %s",
(unsigned long long)log, status.ToString().c_str());
status = Status::OK(); // Keep going with rest of file
}
}
delete lfile;
// Do not record a version edit for this conversion to a Table
// since ExtractMetaData() will also generate edits.
FileMetaData meta;
meta.number = next_file_number_++;
Iterator* iter = mem->NewIterator();
status = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
delete iter;
mem->Unref();
mem = nullptr;
if (status.ok()) {
if (meta.file_size > 0) {
table_numbers_.push_back(meta.number);
}
}
Log(options_.info_log, "Log #%llu: %d ops saved to Table #%llu %s",
(unsigned long long)log, counter, (unsigned long long)meta.number,
status.ToString().c_str());
return status;
}
void ExtractMetaData() {
for (size_t i = 0; i < table_numbers_.size(); i++) {
ScanTable(table_numbers_[i]);
}
}
Iterator* NewTableIterator(const FileMetaData& meta) {
// Same as compaction iterators: if paranoid_checks are on, turn
// on checksum verification.
ReadOptions r;
r.verify_checksums = options_.paranoid_checks;
return table_cache_->NewIterator(r, meta.number, meta.file_size);
}
void ScanTable(uint64_t number) {
TableInfo t;
t.meta.number = number;
std::string fname = TableFileName(dbname_, number);
Status status = env_->GetFileSize(fname, &t.meta.file_size);
if (!status.ok()) {
// Try alternate file name.
fname = SSTTableFileName(dbname_, number);
Status s2 = env_->GetFileSize(fname, &t.meta.file_size);
if (s2.ok()) {
status = Status::OK();
}
}
if (!status.ok()) {
ArchiveFile(TableFileName(dbname_, number));
ArchiveFile(SSTTableFileName(dbname_, number));
Log(options_.info_log, "Table #%llu: dropped: %s",
(unsigned long long)t.meta.number, status.ToString().c_str());
return;
}
// Extract metadata by scanning through table.
int counter = 0;
Iterator* iter = NewTableIterator(t.meta);
bool empty = true;
ParsedInternalKey parsed;
t.max_sequence = 0;
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
Slice key = iter->key();
if (!ParseInternalKey(key, &parsed)) {
Log(options_.info_log, "Table #%llu: unparsable key %s",
(unsigned long long)t.meta.number, EscapeString(key).c_str());
continue;
}
counter++;
if (empty) {
empty = false;
t.meta.smallest.DecodeFrom(key);
}
t.meta.largest.DecodeFrom(key);
if (parsed.sequence > t.max_sequence) {
t.max_sequence = parsed.sequence;
}
}
if (!iter->status().ok()) {
status = iter->status();
}
delete iter;
Log(options_.info_log, "Table #%llu: %d entries %s",
(unsigned long long)t.meta.number, counter, status.ToString().c_str());
if (status.ok()) {
tables_.push_back(t);
} else {
RepairTable(fname, t); // RepairTable archives input file.
}
}
void RepairTable(const std::string& src, TableInfo t) {
// We will copy src contents to a new table and then rename the
// new table over the source.
// Create builder.
std::string copy = TableFileName(dbname_, next_file_number_++);
WritableFile* file;
Status s = env_->NewWritableFile(copy, &file);
if (!s.ok()) {
return;
}
TableBuilder* builder = new TableBuilder(options_, file);
// Copy data.
Iterator* iter = NewTableIterator(t.meta);
int counter = 0;
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
builder->Add(iter->key(), iter->value());
counter++;
}
delete iter;
ArchiveFile(src);
if (counter == 0) {
builder->Abandon(); // Nothing to save
} else {
s = builder->Finish();
if (s.ok()) {
t.meta.file_size = builder->FileSize();
}
}
delete builder;
builder = nullptr;
if (s.ok()) {
s = file->Close();
}
delete file;
file = nullptr;
if (counter > 0 && s.ok()) {
std::string orig = TableFileName(dbname_, t.meta.number);
s = env_->RenameFile(copy, orig);
if (s.ok()) {
Log(options_.info_log, "Table #%llu: %d entries repaired",
(unsigned long long)t.meta.number, counter);
tables_.push_back(t);
}
}
if (!s.ok()) {
env_->RemoveFile(copy);
}
}
Status WriteDescriptor() {
std::string tmp = TempFileName(dbname_, 1);
WritableFile* file;
Status status = env_->NewWritableFile(tmp, &file);
if (!status.ok()) {
return status;
}
SequenceNumber max_sequence = 0;
for (size_t i = 0; i < tables_.size(); i++) {
if (max_sequence < tables_[i].max_sequence) {
max_sequence = tables_[i].max_sequence;
}
}
edit_.SetComparatorName(icmp_.user_comparator()->Name());
edit_.SetLogNumber(0);
edit_.SetNextFile(next_file_number_);
edit_.SetLastSequence(max_sequence);
for (size_t i = 0; i < tables_.size(); i++) {
// TODO(opt): separate out into multiple levels
const TableInfo& t = tables_[i];
edit_.AddFile(0, t.meta.number, t.meta.file_size, t.meta.smallest,
t.meta.largest);
}
// std::fprintf(stderr,
// "NewDescriptor:\n%s\n", edit_.DebugString().c_str());
{
log::Writer log(file);
std::string record;
edit_.EncodeTo(&record);
status = log.AddRecord(record);
}
if (status.ok()) {
status = file->Close();
}
delete file;
file = nullptr;
if (!status.ok()) {
env_->RemoveFile(tmp);
} else {
// Discard older manifests
for (size_t i = 0; i < manifests_.size(); i++) {
ArchiveFile(dbname_ + "/" + manifests_[i]);
}
// Install new manifest
status = env_->RenameFile(tmp, DescriptorFileName(dbname_, 1));
if (status.ok()) {
status = SetCurrentFile(env_, dbname_, 1);
} else {
env_->RemoveFile(tmp);
}
}
return status;
}
void ArchiveFile(const std::string& fname) {
// Move into another directory. E.g., for
// dir/foo
// rename to
// dir/lost/foo
const char* slash = strrchr(fname.c_str(), '/');
std::string new_dir;
if (slash != nullptr) {
new_dir.assign(fname.data(), slash - fname.data());
}
new_dir.append("/lost");
env_->CreateDir(new_dir); // Ignore error
std::string new_file = new_dir;
new_file.append("/");
new_file.append((slash == nullptr) ? fname.c_str() : slash + 1);
Status s = env_->RenameFile(fname, new_file);
Log(options_.info_log, "Archiving %s: %s\n", fname.c_str(),
s.ToString().c_str());
}
const std::string dbname_;
Env* const env_;
InternalKeyComparator const icmp_;
InternalFilterPolicy const ipolicy_;
const Options options_;
bool owns_info_log_;
bool owns_cache_;
TableCache* table_cache_;
VersionEdit edit_;
std::vector<std::string> manifests_;
std::vector<uint64_t> table_numbers_;
std::vector<uint64_t> logs_;
std::vector<TableInfo> tables_;
uint64_t next_file_number_;
};
} // namespace
Status RepairDB(const std::string& dbname, const Options& options) {
Repairer repairer(dbname, options);
return repairer.Run();
}
} // namespace leveldb
+380
View File
@@ -0,0 +1,380 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_DB_SKIPLIST_H_
#define STORAGE_LEVELDB_DB_SKIPLIST_H_
// Thread safety
// -------------
//
// Writes require external synchronization, most likely a mutex.
// Reads require a guarantee that the SkipList will not be destroyed
// while the read is in progress. Apart from that, reads progress
// without any internal locking or synchronization.
//
// Invariants:
//
// (1) Allocated nodes are never deleted until the SkipList is
// destroyed. This is trivially guaranteed by the code since we
// never delete any skip list nodes.
//
// (2) The contents of a Node except for the next/prev pointers are
// immutable after the Node has been linked into the SkipList.
// Only Insert() modifies the list, and it is careful to initialize
// a node and use release-stores to publish the nodes in one or
// more lists.
//
// ... prev vs. next pointer ordering ...
#include <atomic>
#include <cassert>
#include <cstdlib>
#include "util/arena.h"
#include "util/random.h"
namespace leveldb {
template <typename Key, class Comparator>
class SkipList {
private:
struct Node;
public:
// Create a new SkipList object that will use "cmp" for comparing keys,
// and will allocate memory using "*arena". Objects allocated in the arena
// must remain allocated for the lifetime of the skiplist object.
explicit SkipList(Comparator cmp, Arena* arena);
SkipList(const SkipList&) = delete;
SkipList& operator=(const SkipList&) = delete;
// Insert key into the list.
// REQUIRES: nothing that compares equal to key is currently in the list.
void Insert(const Key& key);
// Returns true iff an entry that compares equal to key is in the list.
bool Contains(const Key& key) const;
// Iteration over the contents of a skip list
class Iterator {
public:
// Initialize an iterator over the specified list.
// The returned iterator is not valid.
explicit Iterator(const SkipList* list);
// Returns true iff the iterator is positioned at a valid node.
bool Valid() const;
// Returns the key at the current position.
// REQUIRES: Valid()
const Key& key() const;
// Advances to the next position.
// REQUIRES: Valid()
void Next();
// Advances to the previous position.
// REQUIRES: Valid()
void Prev();
// Advance to the first entry with a key >= target
void Seek(const Key& target);
// Position at the first entry in list.
// Final state of iterator is Valid() iff list is not empty.
void SeekToFirst();
// Position at the last entry in list.
// Final state of iterator is Valid() iff list is not empty.
void SeekToLast();
private:
const SkipList* list_;
Node* node_;
// Intentionally copyable
};
private:
enum { kMaxHeight = 12 };
inline int GetMaxHeight() const {
return max_height_.load(std::memory_order_relaxed);
}
Node* NewNode(const Key& key, int height);
int RandomHeight();
bool Equal(const Key& a, const Key& b) const { return (compare_(a, b) == 0); }
// Return true if key is greater than the data stored in "n"
bool KeyIsAfterNode(const Key& key, Node* n) const;
// Return the earliest node that comes at or after key.
// Return nullptr if there is no such node.
//
// If prev is non-null, fills prev[level] with pointer to previous
// node at "level" for every level in [0..max_height_-1].
Node* FindGreaterOrEqual(const Key& key, Node** prev) const;
// Return the latest node with a key < key.
// Return head_ if there is no such node.
Node* FindLessThan(const Key& key) const;
// Return the last node in the list.
// Return head_ if list is empty.
Node* FindLast() const;
// Immutable after construction
Comparator const compare_;
Arena* const arena_; // Arena used for allocations of nodes
Node* const head_;
// Modified only by Insert(). Read racily by readers, but stale
// values are ok.
std::atomic<int> max_height_; // Height of the entire list
// Read/written only by Insert().
Random rnd_;
};
// Implementation details follow
template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
explicit Node(const Key& k) : key(k) {}
Key const key;
// Accessors/mutators for links. Wrapped in methods so we can
// add the appropriate barriers as necessary.
Node* Next(int n) {
assert(n >= 0);
// Use an 'acquire load' so that we observe a fully initialized
// version of the returned Node.
return next_[n].load(std::memory_order_acquire);
}
void SetNext(int n, Node* x) {
assert(n >= 0);
// Use a 'release store' so that anybody who reads through this
// pointer observes a fully initialized version of the inserted node.
next_[n].store(x, std::memory_order_release);
}
// No-barrier variants that can be safely used in a few locations.
Node* NoBarrier_Next(int n) {
assert(n >= 0);
return next_[n].load(std::memory_order_relaxed);
}
void NoBarrier_SetNext(int n, Node* x) {
assert(n >= 0);
next_[n].store(x, std::memory_order_relaxed);
}
private:
// Array of length equal to the node height. next_[0] is lowest level link.
std::atomic<Node*> next_[1];
};
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node* SkipList<Key, Comparator>::NewNode(
const Key& key, int height) {
char* const node_memory = arena_->AllocateAligned(
sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
return new (node_memory) Node(key);
}
template <typename Key, class Comparator>
inline SkipList<Key, Comparator>::Iterator::Iterator(const SkipList* list) {
list_ = list;
node_ = nullptr;
}
template <typename Key, class Comparator>
inline bool SkipList<Key, Comparator>::Iterator::Valid() const {
return node_ != nullptr;
}
template <typename Key, class Comparator>
inline const Key& SkipList<Key, Comparator>::Iterator::key() const {
assert(Valid());
return node_->key;
}
template <typename Key, class Comparator>
inline void SkipList<Key, Comparator>::Iterator::Next() {
assert(Valid());
node_ = node_->Next(0);
}
template <typename Key, class Comparator>
inline void SkipList<Key, Comparator>::Iterator::Prev() {
// Instead of using explicit "prev" links, we just search for the
// last node that falls before key.
assert(Valid());
node_ = list_->FindLessThan(node_->key);
if (node_ == list_->head_) {
node_ = nullptr;
}
}
template <typename Key, class Comparator>
inline void SkipList<Key, Comparator>::Iterator::Seek(const Key& target) {
node_ = list_->FindGreaterOrEqual(target, nullptr);
}
template <typename Key, class Comparator>
inline void SkipList<Key, Comparator>::Iterator::SeekToFirst() {
node_ = list_->head_->Next(0);
}
template <typename Key, class Comparator>
inline void SkipList<Key, Comparator>::Iterator::SeekToLast() {
node_ = list_->FindLast();
if (node_ == list_->head_) {
node_ = nullptr;
}
}
template <typename Key, class Comparator>
int SkipList<Key, Comparator>::RandomHeight() {
// Increase height with probability 1 in kBranching
static const unsigned int kBranching = 4;
int height = 1;
while (height < kMaxHeight && rnd_.OneIn(kBranching)) {
height++;
}
assert(height > 0);
assert(height <= kMaxHeight);
return height;
}
template <typename Key, class Comparator>
bool SkipList<Key, Comparator>::KeyIsAfterNode(const Key& key, Node* n) const {
// null n is considered infinite
return (n != nullptr) && (compare_(n->key, key) < 0);
}
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,
Node** prev) const {
Node* x = head_;
int level = GetMaxHeight() - 1;
while (true) {
Node* next = x->Next(level);
if (KeyIsAfterNode(key, next)) {
// Keep searching in this list
x = next;
} else {
if (prev != nullptr) prev[level] = x;
if (level == 0) {
return next;
} else {
// Switch to next list
level--;
}
}
}
}
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindLessThan(const Key& key) const {
Node* x = head_;
int level = GetMaxHeight() - 1;
while (true) {
assert(x == head_ || compare_(x->key, key) < 0);
Node* next = x->Next(level);
if (next == nullptr || compare_(next->key, key) >= 0) {
if (level == 0) {
return x;
} else {
// Switch to next list
level--;
}
} else {
x = next;
}
}
}
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node* SkipList<Key, Comparator>::FindLast()
const {
Node* x = head_;
int level = GetMaxHeight() - 1;
while (true) {
Node* next = x->Next(level);
if (next == nullptr) {
if (level == 0) {
return x;
} else {
// Switch to next list
level--;
}
} else {
x = next;
}
}
}
template <typename Key, class Comparator>
SkipList<Key, Comparator>::SkipList(Comparator cmp, Arena* arena)
: compare_(cmp),
arena_(arena),
head_(NewNode(0 /* any key will do */, kMaxHeight)),
max_height_(1),
rnd_(0xdeadbeef) {
for (int i = 0; i < kMaxHeight; i++) {
head_->SetNext(i, nullptr);
}
}
template <typename Key, class Comparator>
void SkipList<Key, Comparator>::Insert(const Key& key) {
// TODO(opt): We can use a barrier-free variant of FindGreaterOrEqual()
// here since Insert() is externally synchronized.
Node* prev[kMaxHeight];
Node* x = FindGreaterOrEqual(key, prev);
// Our data structure does not allow duplicate insertion
assert(x == nullptr || !Equal(key, x->key));
int height = RandomHeight();
if (height > GetMaxHeight()) {
for (int i = GetMaxHeight(); i < height; i++) {
prev[i] = head_;
}
// It is ok to mutate max_height_ without any synchronization
// with concurrent readers. A concurrent reader that observes
// the new value of max_height_ will see either the old value of
// new level pointers from head_ (nullptr), or a new value set in
// the loop below. In the former case the reader will
// immediately drop to the next level since nullptr sorts after all
// keys. In the latter case the reader will use the new node.
max_height_.store(height, std::memory_order_relaxed);
}
x = NewNode(key, height);
for (int i = 0; i < height; i++) {
// NoBarrier_SetNext() suffices since we will add a barrier when
// we publish a pointer to "x" in prev[i].
x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
prev[i]->SetNext(i, x);
}
}
template <typename Key, class Comparator>
bool SkipList<Key, Comparator>::Contains(const Key& key) const {
Node* x = FindGreaterOrEqual(key, nullptr);
if (x != nullptr && Equal(key, x->key)) {
return true;
} else {
return false;
}
}
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_SKIPLIST_H_
@@ -0,0 +1,368 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/skiplist.h"
#include <atomic>
#include <set>
#include "gtest/gtest.h"
#include "leveldb/env.h"
#include "port/port.h"
#include "port/thread_annotations.h"
#include "util/arena.h"
#include "util/hash.h"
#include "util/random.h"
#include "util/testutil.h"
namespace leveldb {
typedef uint64_t Key;
struct Comparator {
int operator()(const Key& a, const Key& b) const {
if (a < b) {
return -1;
} else if (a > b) {
return +1;
} else {
return 0;
}
}
};
TEST(SkipTest, Empty) {
Arena arena;
Comparator cmp;
SkipList<Key, Comparator> list(cmp, &arena);
ASSERT_TRUE(!list.Contains(10));
SkipList<Key, Comparator>::Iterator iter(&list);
ASSERT_TRUE(!iter.Valid());
iter.SeekToFirst();
ASSERT_TRUE(!iter.Valid());
iter.Seek(100);
ASSERT_TRUE(!iter.Valid());
iter.SeekToLast();
ASSERT_TRUE(!iter.Valid());
}
TEST(SkipTest, InsertAndLookup) {
const int N = 2000;
const int R = 5000;
Random rnd(1000);
std::set<Key> keys;
Arena arena;
Comparator cmp;
SkipList<Key, Comparator> list(cmp, &arena);
for (int i = 0; i < N; i++) {
Key key = rnd.Next() % R;
if (keys.insert(key).second) {
list.Insert(key);
}
}
for (int i = 0; i < R; i++) {
if (list.Contains(i)) {
ASSERT_EQ(keys.count(i), 1);
} else {
ASSERT_EQ(keys.count(i), 0);
}
}
// Simple iterator tests
{
SkipList<Key, Comparator>::Iterator iter(&list);
ASSERT_TRUE(!iter.Valid());
iter.Seek(0);
ASSERT_TRUE(iter.Valid());
ASSERT_EQ(*(keys.begin()), iter.key());
iter.SeekToFirst();
ASSERT_TRUE(iter.Valid());
ASSERT_EQ(*(keys.begin()), iter.key());
iter.SeekToLast();
ASSERT_TRUE(iter.Valid());
ASSERT_EQ(*(keys.rbegin()), iter.key());
}
// Forward iteration test
for (int i = 0; i < R; i++) {
SkipList<Key, Comparator>::Iterator iter(&list);
iter.Seek(i);
// Compare against model iterator
std::set<Key>::iterator model_iter = keys.lower_bound(i);
for (int j = 0; j < 3; j++) {
if (model_iter == keys.end()) {
ASSERT_TRUE(!iter.Valid());
break;
} else {
ASSERT_TRUE(iter.Valid());
ASSERT_EQ(*model_iter, iter.key());
++model_iter;
iter.Next();
}
}
}
// Backward iteration test
{
SkipList<Key, Comparator>::Iterator iter(&list);
iter.SeekToLast();
// Compare against model iterator
for (std::set<Key>::reverse_iterator model_iter = keys.rbegin();
model_iter != keys.rend(); ++model_iter) {
ASSERT_TRUE(iter.Valid());
ASSERT_EQ(*model_iter, iter.key());
iter.Prev();
}
ASSERT_TRUE(!iter.Valid());
}
}
// We want to make sure that with a single writer and multiple
// concurrent readers (with no synchronization other than when a
// reader's iterator is created), the reader always observes all the
// data that was present in the skip list when the iterator was
// constructed. Because insertions are happening concurrently, we may
// also observe new values that were inserted since the iterator was
// constructed, but we should never miss any values that were present
// at iterator construction time.
//
// We generate multi-part keys:
// <key,gen,hash>
// where:
// key is in range [0..K-1]
// gen is a generation number for key
// hash is hash(key,gen)
//
// The insertion code picks a random key, sets gen to be 1 + the last
// generation number inserted for that key, and sets hash to Hash(key,gen).
//
// At the beginning of a read, we snapshot the last inserted
// generation number for each key. We then iterate, including random
// calls to Next() and Seek(). For every key we encounter, we
// check that it is either expected given the initial snapshot or has
// been concurrently added since the iterator started.
class ConcurrentTest {
private:
static constexpr uint32_t K = 4;
static uint64_t key(Key key) { return (key >> 40); }
static uint64_t gen(Key key) { return (key >> 8) & 0xffffffffu; }
static uint64_t hash(Key key) { return key & 0xff; }
static uint64_t HashNumbers(uint64_t k, uint64_t g) {
uint64_t data[2] = {k, g};
return Hash(reinterpret_cast<char*>(data), sizeof(data), 0);
}
static Key MakeKey(uint64_t k, uint64_t g) {
static_assert(sizeof(Key) == sizeof(uint64_t), "");
assert(k <= K); // We sometimes pass K to seek to the end of the skiplist
assert(g <= 0xffffffffu);
return ((k << 40) | (g << 8) | (HashNumbers(k, g) & 0xff));
}
static bool IsValidKey(Key k) {
return hash(k) == (HashNumbers(key(k), gen(k)) & 0xff);
}
static Key RandomTarget(Random* rnd) {
switch (rnd->Next() % 10) {
case 0:
// Seek to beginning
return MakeKey(0, 0);
case 1:
// Seek to end
return MakeKey(K, 0);
default:
// Seek to middle
return MakeKey(rnd->Next() % K, 0);
}
}
// Per-key generation
struct State {
std::atomic<int> generation[K];
void Set(int k, int v) {
generation[k].store(v, std::memory_order_release);
}
int Get(int k) { return generation[k].load(std::memory_order_acquire); }
State() {
for (int k = 0; k < K; k++) {
Set(k, 0);
}
}
};
// Current state of the test
State current_;
Arena arena_;
// SkipList is not protected by mu_. We just use a single writer
// thread to modify it.
SkipList<Key, Comparator> list_;
public:
ConcurrentTest() : list_(Comparator(), &arena_) {}
// REQUIRES: External synchronization
void WriteStep(Random* rnd) {
const uint32_t k = rnd->Next() % K;
const intptr_t g = current_.Get(k) + 1;
const Key key = MakeKey(k, g);
list_.Insert(key);
current_.Set(k, g);
}
void ReadStep(Random* rnd) {
// Remember the initial committed state of the skiplist.
State initial_state;
for (int k = 0; k < K; k++) {
initial_state.Set(k, current_.Get(k));
}
Key pos = RandomTarget(rnd);
SkipList<Key, Comparator>::Iterator iter(&list_);
iter.Seek(pos);
while (true) {
Key current;
if (!iter.Valid()) {
current = MakeKey(K, 0);
} else {
current = iter.key();
ASSERT_TRUE(IsValidKey(current)) << current;
}
ASSERT_LE(pos, current) << "should not go backwards";
// Verify that everything in [pos,current) was not present in
// initial_state.
while (pos < current) {
ASSERT_LT(key(pos), K) << pos;
// Note that generation 0 is never inserted, so it is ok if
// <*,0,*> is missing.
ASSERT_TRUE((gen(pos) == 0) ||
(gen(pos) > static_cast<Key>(initial_state.Get(key(pos)))))
<< "key: " << key(pos) << "; gen: " << gen(pos)
<< "; initgen: " << initial_state.Get(key(pos));
// Advance to next key in the valid key space
if (key(pos) < key(current)) {
pos = MakeKey(key(pos) + 1, 0);
} else {
pos = MakeKey(key(pos), gen(pos) + 1);
}
}
if (!iter.Valid()) {
break;
}
if (rnd->Next() % 2) {
iter.Next();
pos = MakeKey(key(pos), gen(pos) + 1);
} else {
Key new_target = RandomTarget(rnd);
if (new_target > pos) {
pos = new_target;
iter.Seek(new_target);
}
}
}
}
};
// Needed when building in C++11 mode.
constexpr uint32_t ConcurrentTest::K;
// Simple test that does single-threaded testing of the ConcurrentTest
// scaffolding.
TEST(SkipTest, ConcurrentWithoutThreads) {
ConcurrentTest test;
Random rnd(test::RandomSeed());
for (int i = 0; i < 10000; i++) {
test.ReadStep(&rnd);
test.WriteStep(&rnd);
}
}
class TestState {
public:
ConcurrentTest t_;
int seed_;
std::atomic<bool> quit_flag_;
enum ReaderState { STARTING, RUNNING, DONE };
explicit TestState(int s)
: seed_(s), quit_flag_(false), state_(STARTING), state_cv_(&mu_) {}
void Wait(ReaderState s) LOCKS_EXCLUDED(mu_) {
mu_.Lock();
while (state_ != s) {
state_cv_.Wait();
}
mu_.Unlock();
}
void Change(ReaderState s) LOCKS_EXCLUDED(mu_) {
mu_.Lock();
state_ = s;
state_cv_.Signal();
mu_.Unlock();
}
private:
port::Mutex mu_;
ReaderState state_ GUARDED_BY(mu_);
port::CondVar state_cv_ GUARDED_BY(mu_);
};
static void ConcurrentReader(void* arg) {
TestState* state = reinterpret_cast<TestState*>(arg);
Random rnd(state->seed_);
int64_t reads = 0;
state->Change(TestState::RUNNING);
while (!state->quit_flag_.load(std::memory_order_acquire)) {
state->t_.ReadStep(&rnd);
++reads;
}
state->Change(TestState::DONE);
}
static void RunConcurrent(int run) {
const int seed = test::RandomSeed() + (run * 100);
Random rnd(seed);
const int N = 1000;
const int kSize = 1000;
for (int i = 0; i < N; i++) {
if ((i % 100) == 0) {
std::fprintf(stderr, "Run %d of %d\n", i, N);
}
TestState state(seed + 1);
Env::Default()->Schedule(ConcurrentReader, &state);
state.Wait(TestState::RUNNING);
for (int i = 0; i < kSize; i++) {
state.t_.WriteStep(&rnd);
}
state.quit_flag_.store(true, std::memory_order_release);
state.Wait(TestState::DONE);
}
}
TEST(SkipTest, Concurrent1) { RunConcurrent(1); }
TEST(SkipTest, Concurrent2) { RunConcurrent(2); }
TEST(SkipTest, Concurrent3) { RunConcurrent(3); }
TEST(SkipTest, Concurrent4) { RunConcurrent(4); }
TEST(SkipTest, Concurrent5) { RunConcurrent(5); }
} // namespace leveldb
@@ -0,0 +1,95 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_DB_SNAPSHOT_H_
#define STORAGE_LEVELDB_DB_SNAPSHOT_H_
#include "db/dbformat.h"
#include "leveldb/db.h"
namespace leveldb {
class SnapshotList;
// Snapshots are kept in a doubly-linked list in the DB.
// Each SnapshotImpl corresponds to a particular sequence number.
class SnapshotImpl : public Snapshot {
public:
SnapshotImpl(SequenceNumber sequence_number)
: sequence_number_(sequence_number) {}
SequenceNumber sequence_number() const { return sequence_number_; }
private:
friend class SnapshotList;
// SnapshotImpl is kept in a doubly-linked circular list. The SnapshotList
// implementation operates on the next/previous fields directly.
SnapshotImpl* prev_;
SnapshotImpl* next_;
const SequenceNumber sequence_number_;
#if !defined(NDEBUG)
SnapshotList* list_ = nullptr;
#endif // !defined(NDEBUG)
};
class SnapshotList {
public:
SnapshotList() : head_(0) {
head_.prev_ = &head_;
head_.next_ = &head_;
}
bool empty() const { return head_.next_ == &head_; }
SnapshotImpl* oldest() const {
assert(!empty());
return head_.next_;
}
SnapshotImpl* newest() const {
assert(!empty());
return head_.prev_;
}
// Creates a SnapshotImpl and appends it to the end of the list.
SnapshotImpl* New(SequenceNumber sequence_number) {
assert(empty() || newest()->sequence_number_ <= sequence_number);
SnapshotImpl* snapshot = new SnapshotImpl(sequence_number);
#if !defined(NDEBUG)
snapshot->list_ = this;
#endif // !defined(NDEBUG)
snapshot->next_ = &head_;
snapshot->prev_ = head_.prev_;
snapshot->prev_->next_ = snapshot;
snapshot->next_->prev_ = snapshot;
return snapshot;
}
// Removes a SnapshotImpl from this list.
//
// The snapshot must have been created by calling New() on this list.
//
// The snapshot pointer should not be const, because its memory is
// deallocated. However, that would force us to change DB::ReleaseSnapshot(),
// which is in the API, and currently takes a const Snapshot.
void Delete(const SnapshotImpl* snapshot) {
#if !defined(NDEBUG)
assert(snapshot->list_ == this);
#endif // !defined(NDEBUG)
snapshot->prev_->next_ = snapshot->next_;
snapshot->next_->prev_ = snapshot->prev_;
delete snapshot;
}
private:
// Dummy head of doubly-linked list of snapshots
SnapshotImpl head_;
};
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_SNAPSHOT_H_

Some files were not shown because too many files have changed in this diff Show More