Compare commits
60 Commits
foundry
...
e340a68cff
| Author | SHA1 | Date | |
|---|---|---|---|
| e340a68cff | |||
| b82f6ab305 | |||
| d233593a9b | |||
| c7e748e382 | |||
| 21ac6bf9fa | |||
| 6497042f97 | |||
| 735650358a | |||
| d653d58741 | |||
| 51235ebaf2 | |||
| 61bc68dd3c | |||
| 76674f410e | |||
| 2933dcd3ea | |||
| 91944f97ba | |||
| f311a0c8e9 | |||
| 99f8e5929b | |||
| 151e6d3e13 | |||
| abb0405035 | |||
| 66e7e93bae | |||
| 474b2b6df7 | |||
| 437a27ad28 | |||
| 7ac9649768 | |||
| b1da5ff6f2 | |||
| 53bff7a4a6 | |||
| add059a902 | |||
| a3b6271bf3 | |||
| 43459113d5 | |||
| 04093d752e | |||
| ef00bd029e | |||
| d7779f3c48 | |||
| b83213fc0b | |||
| dcbb9f6c9c | |||
| ef4539ee39 | |||
| fea5de6596 | |||
| 4079a40d6b | |||
| 57eb581ae7 | |||
| b011a65510 | |||
| 3dff7555e5 | |||
| 5ef47a483f | |||
| f824286243 | |||
| 942c395f59 | |||
| 98dcb0749d | |||
| 3946a8116c | |||
| 7384e6cdcf | |||
| 5179ecba91 | |||
| d9e1721459 | |||
| 998951bafc | |||
| 6aa65be7a0 | |||
| 2f9410180c | |||
| 307307d271 | |||
| a6ae10beba | |||
| 19297c4bd7 | |||
| 6e58e06058 | |||
| 172e98f663 | |||
| 00fb647f0f | |||
| e2ffb67d35 | |||
| 948dba6032 | |||
| 0df8a4e89d | |||
| 48e0c5db3c | |||
| d099e32fcc | |||
| 442cae2598 |
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
{{#*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 class="actor-sheet ActorSheet" data-tooltip-class="DSA41">
|
||||
<div class="grid5 gap">
|
||||
{{>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")}}
|
||||
{{>editable-input type="number" name="system.sozialstatus" value=actor.system.sozialstatus placeholder=(localize "DSA41.sozialstatus")}}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<img class="character-image" src="{{ actor.img }}" title="{{ actor.name }}" {{#if editable}}data-edit="img"{{/if}}>
|
||||
<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}}%;">
|
||||
{{>editable-input type="number" name="system.lebenspunkte.aktuell" value=actor.system.lebenspunkte.aktuell}} / <span>{{actor.system.computed.lebenspunkte.max}}</span>
|
||||
</div>
|
||||
<div class="bar ausdauer" style="--bar-percentage: {{actor.system.computed.ausdauer.prozent}}%;">
|
||||
{{>editable-input type="number" name="system.ausdauer.aktuell" value=actor.system.ausdauer.aktuell}} / <span>{{actor.system.computed.ausdauer.max}}</span>
|
||||
</div>
|
||||
<div class="bar astralenergie" style="--bar-percentage: {{actor.system.computed.astralenergie.prozent}}%;">
|
||||
{{>editable-input type="number" name="system.astralenergie.aktuell" value=actor.system.astralenergie.aktuell}} / <span>{{actor.system.computed.astralenergie.max}}</span>
|
||||
</div>
|
||||
</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.character.eigenschaften"}}</a>
|
||||
</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.character.talente"}}</a>
|
||||
</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.character.inventar"}}</a>
|
||||
</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.character.kampf"}}</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="tab {{#if (eq tabGroups.primary 'tab1')}}active{{/if}}" data-group="primary" data-tab="tab1">
|
||||
<div class="Abenteuerpunkte">
|
||||
<span>{{localize "DSA41.abenteuerpunkte"}}:</span>
|
||||
{{>editable-input type="number" name="system.abenteuerpunkte.ausgegeben" value=actor.system.abenteuerpunkte.ausgegeben}}
|
||||
/
|
||||
{{>editable-input type="number" name="system.abenteuerpunkte.gesamt" value=actor.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>
|
||||
{{>editable-input type="number" name=(concat "system.attributes." @key ".initial") value=(lookup this "initial")}}
|
||||
{{>editable-input type="number" name=(concat "system.attributes." @key ".advancement") value=(lookup this "advancement")}}
|
||||
{{>editable-input type="number" name=(concat "system.attributes." @key ".modifier") value=(lookup this "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>{{>editable-input type="number" name="system.lebenspunkte.modifikator" value=actor.system.lebenspunkte.modifikator}}</span>
|
||||
<span>{{>editable-input type="number" name="system.lebenspunkte.zukauf" value=actor.system.lebenspunkte.zukauf}}</span>
|
||||
<span>{{>editable-input type="number" name="system.lebenspunkte.verlust" value=actor.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>{{>editable-input type="number" name="system.ausdauer.modifikator" value=actor.system.ausdauer.modifikator}}</span>
|
||||
<span>{{>editable-input type="number" name="system.ausdauer.zukauf" value=actor.system.ausdauer.zukauf}}</span>
|
||||
<span>{{>editable-input type="number" name="system.ausdauer.verlust" value=actor.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>{{>editable-input type="number" name="system.astralenergie.modifikator" value=actor.system.astralenergie.modifikator}}</span>
|
||||
<span>{{>editable-input type="number" name="system.astralenergie.zukauf" value=actor.system.astralenergie.zukauf}}</span>
|
||||
<span>{{>editable-input type="number" name="system.astralenergie.verlust" value=actor.system.astralenergie.verlust}}</span>
|
||||
<span>{{actor.system.computed.astralenergie.max}}</span>
|
||||
</div>
|
||||
|
||||
<div class="list-item">
|
||||
<span>{{localize "DSA41.basiswerte.karmalenergie"}}</span>
|
||||
<span>{{>editable-input type="number" name="system.karmalenergie" value=actor.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>{{>editable-input type="number" name="system.magieresistenz.modifikator" value=actor.system.magieresistenz.modifikator}}</span>
|
||||
<span>{{>editable-input type="number" name="system.magieresistenz.zukauf" value=actor.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>{{>editable-input type="number" name="system.modifikator_initiative" value=actor.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>{{>editable-input type="number" name="system.modifikator_attacke" value=actor.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>{{>editable-input type="number" name="system.modifikator_parade" value=actor.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>{{>editable-input type="number" name="system.modifikator_fernkampf" value=actor.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 'tab2')}}active{{/if}}" data-group="primary" data-tab="tab2">
|
||||
{{#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 name=(concat name "system.talentwert") 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>
|
||||
<div>{{>editable-input type="number" name=(concat name "system.parade") data-name="system.parade" value=system.parade}}</div>
|
||||
<div class="center">{{lookup (lookup @root.actor.system.computed.kampf.talente name) "attacke"}}</div>
|
||||
<div class="center">{{lookup (lookup @root.actor.system.computed.kampf.talente name) "parade"}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab {{#if (eq tabGroups.primary 'tab3')}}active{{/if}}" data-group="primary" data-tab="tab3">
|
||||
<div class="currency">
|
||||
{{>editable-input type="number" name="system.currency.dukaten" value=actor.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>
|
||||
{{>editable-input type="number" name="system.currency.silbertaler" value=actor.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>
|
||||
{{>editable-input type="number" name="system.currency.heller" value=actor.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>
|
||||
{{>editable-input type="number" name="system.currency.kreuzer" value=actor.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"}}</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
{{#unless (ne actor.itemTypes.Bewaffnung.length 0)}}
|
||||
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#each actor.itemTypes.Bewaffnung}}
|
||||
<div class="list-item" 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}}</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"}}</div>
|
||||
</div>
|
||||
|
||||
{{#unless (ne actor.itemTypes.Ruestung.length 0)}}
|
||||
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#each actor.itemTypes.Ruestung}}
|
||||
<div class="list-item" 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}}</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"}}</div>
|
||||
</div>
|
||||
|
||||
{{#unless (ne actor.itemTypes.Gegenstand.length 0)}}
|
||||
<div class="list-item">{{localize "DSA41.list_empty"}}</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#each actor.itemTypes.Gegenstand}}
|
||||
<div class="list-item" 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}}</div>
|
||||
<div class="center fas fa-trash" data-action="item-delete"></div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab Kampf {{#if (eq tabGroups.primary 'tab4')}}active{{/if}}" data-group="primary" data-tab="tab4">
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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 |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="parade_dialog">
|
||||
<span class="colspan2">{{localize "DSA41.kampf.modifikator"}}</span>
|
||||
{{>editable-input type="number" name="modifikator" class="colspan2" 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>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="parade_dialog">
|
||||
<span class="colspan2">{{localize "DSA41.kampf.modifikator"}}</span>
|
||||
{{>editable-input type="number" name="modifikator" class="colspan2" 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>
|
||||
@@ -0,0 +1,59 @@
|
||||
<div class="fernkampf_angriff_dialog">
|
||||
<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>
|
||||
{{>editable-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>
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="fernkampf_trefferpunkte_dialog">
|
||||
<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>
|
||||
{{>editable-input type="number" name="modifikator" value=(lookup formData "modifikator") }}
|
||||
|
||||
<span>{{localize "DSA41.kampf.crit"}}</span>
|
||||
{{>editable-input type="checkbox" name="crit" class="center" value=(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>
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="parade_dialog">
|
||||
<span>{{localize "DSA41.kampf.modifikator"}}</span>
|
||||
{{>editable-input type="number" name="modifikator" value=(lookup formData "modifikator") }}
|
||||
|
||||
<span>{{localize "DSA41.kampf.crit"}}</span>
|
||||
{{>editable-input type="checkbox" name="crit" class="center" value=(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>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="talent_dialog">
|
||||
<span class="colspan2">{{localize "DSA41.kampf.modifikator"}}</span>
|
||||
{{>editable-input class="colspan2" type="number" name="modifikator" value=(lookup formData "modifikator") }}
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="trefferpunkte_dialog">
|
||||
<span>{{localize "DSA41.kampf.modifikator"}}</span>
|
||||
{{>editable-input type="number" name="modifikator" value=(lookup formData "modifikator") }}
|
||||
|
||||
<span>{{localize "DSA41.kampf.crit"}}</span>
|
||||
{{>editable-input type="checkbox" name="crit" class="center" value=(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>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="trefferzone_dialog">
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,130 @@
|
||||
<div class="Bewaffnung {{ cssClass }}">
|
||||
<div class="row">
|
||||
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" {{#if editable}}data-edit="img"{{/if}}>
|
||||
<div class="col">
|
||||
{{>editable-input type="text" name="name" value=item.name placeholder=(localize "DSA41.name")}}
|
||||
<div class="row gap">
|
||||
{{>editable-input type="number" name="system.gewicht" value=item.system.gewicht placeholder=(localize "DSA41.weight")}}
|
||||
{{>editable-input type="number" name="system.preis" value=item.system.preis placeholder=(localize "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>
|
||||
{{>editable-input type="checkbox" name="system.nahkampfwaffe.aktiv" value=item.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>
|
||||
{{>editable-input type="checkbox" name="system.parierwaffe.aktiv" value=item.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>
|
||||
{{>editable-input type="checkbox" name="system.schild.aktiv" value=item.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>
|
||||
{{>editable-input type="checkbox" name="system.fernkampfwaffe.aktiv" value=item.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">
|
||||
{{>editable-input type="text" name="system.nahkampfwaffe.basis" value=item.system.nahkampfwaffe.basis placeholder=(localize "DSA41.bewaffnung.nahkampfwaffe.basis")}}
|
||||
{{>editable-input type="number" name="system.nahkampfwaffe.schwellenwert" value=item.system.nahkampfwaffe.schwellenwert placeholder=(localize "DSA41.bewaffnung.nahkampfwaffe.schwellenwert")}}
|
||||
{{>editable-input type="number" name="system.nahkampfwaffe.schadensschritte" value=item.system.nahkampfwaffe.schadensschritte placeholder=(localize "DSA41.bewaffnung.nahkampfwaffe.schadensschritte")}}
|
||||
</div>
|
||||
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.modifikator"}}</span>
|
||||
<div class="grid2">
|
||||
{{>editable-input type="number" name="system.nahkampfwaffe.modifikator_attacke" value=item.system.nahkampfwaffe.modifikator_attacke placeholder=(localize "DSA41.bewaffnung.nahkampfwaffe.attacke")}}
|
||||
{{>editable-input type="number" name="system.nahkampfwaffe.modifikator_parade" value=item.system.nahkampfwaffe.modifikator_parade placeholder=(localize "DSA41.bewaffnung.nahkampfwaffe.parade")}}
|
||||
</div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.initiative"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.nahkampfwaffe.initiative" value=item.system.nahkampfwaffe.initiative}}</div>
|
||||
<span>{{localize "DSA41.bewaffnung.bruchfaktor"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.nahkampfwaffe.bruchfaktor" value=item.system.nahkampfwaffe.bruchfaktor}}</div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.distanzklasse"}}</span>
|
||||
<div>{{>editable-input type="text" name="system.nahkampfwaffe.distanzklasse" value=item.system.nahkampfwaffe.distanzklasse}}</div>
|
||||
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.kampftalente"}}</span>
|
||||
<div>{{>editable-input type="text" name="system.nahkampfwaffe.kampftalente" value=item.system.nahkampfwaffe.kampftalente}}</div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.laenge"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.nahkampfwaffe.laenge" value=item.system.nahkampfwaffe.laenge}}</div>
|
||||
<div class="colspan2"></div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.zweihaendig"}}</span>
|
||||
<div>{{>editable-input type="checkbox" name="system.nahkampfwaffe.zweihaendig" value=item.system.nahkampfwaffe.zweihaendig}}</div>
|
||||
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.werfbar"}}</span>
|
||||
<div>{{>editable-input type="checkbox" name="system.nahkampfwaffe.werfbar" value=item.system.nahkampfwaffe.werfbar}}</div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.improvisiert"}}</span>
|
||||
<div>{{>editable-input type="checkbox" name="system.nahkampfwaffe.improvisiert" value=item.system.nahkampfwaffe.improvisiert}}</div>
|
||||
<span>{{localize "DSA41.bewaffnung.nahkampfwaffe.priviligiert"}}</span>
|
||||
<div>{{>editable-input type="checkbox" name="system.nahkampfwaffe.priviligiert" value=item.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>{{>editable-input type="number" name="system.parierwaffe.initiative" value=item.system.parierwaffe.initiative}}</div>
|
||||
<span>{{localize "DSA41.bewaffnung.parierwaffe.modifikator"}}</span>
|
||||
<div class="grid2">
|
||||
{{>editable-input type="number" name="system.parierwaffe.modifikator_attacke" value=item.system.parierwaffe.modifikator_attacke placeholder=(localize "DSA41.bewaffnung.parierwaffe.attacke")}}
|
||||
{{>editable-input type="number" name="system.parierwaffe.modifikator_parade" value=item.system.parierwaffe.modifikator_parade placeholder=(localize "DSA41.bewaffnung.parierwaffe.parade")}}
|
||||
</div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.bruchfaktor"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.parierwaffe.bruchfaktor" value=item.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>
|
||||
<select name="system.schild.groesse">
|
||||
<option value="klein" {{#if (eq item.system.schild.groesse "klein")}} selected {{/if}}>{{localize "DSA41.bewaffnung.schild.groesse.klein"}}</option>
|
||||
<option value="gross" {{#if (eq item.system.schild.groesse "gross")}} selected {{/if}}>{{localize "DSA41.bewaffnung.schild.groesse.gross"}}</option>
|
||||
<option value="sehr_gross" {{#if (eq item.system.schild.groesse "sehr_gross")}} selected {{/if}}>{{localize "DSA41.bewaffnung.schild.groesse.sehr_gross"}}</option>
|
||||
</select>
|
||||
<span>{{localize "DSA41.bewaffnung.schild.modifikator"}}</span>
|
||||
<div class="grid2">
|
||||
{{>editable-input type="number" name="system.schild.modifikator_attacke" value=item.system.schild.modifikator_attacke placeholder=(localize "DSA41.bewaffnung.schild.attacke")}}
|
||||
{{>editable-input type="number" name="system.schild.modifikator_parade" value=item.system.schild.modifikator_parade placeholder=(localize "DSA41.bewaffnung.schild.parade")}}
|
||||
</div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.schild.initiative"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.schild.initiative" value=item.system.schild.initiative}}</div>
|
||||
<span>{{localize "DSA41.bewaffnung.bruchfaktor"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.schild.bruchfaktor" value=item.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">{{>editable-input type="text" name="system.fernkampfwaffe.basis" value=item.system.fernkampfwaffe.basis}}</div>
|
||||
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.laden"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.fernkampfwaffe.laden" value=item.system.fernkampfwaffe.laden}}</div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.munitionskosten"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.fernkampfwaffe.munitionskosten" value=item.system.fernkampfwaffe.munitionskosten}}</div>
|
||||
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.munitionsgewicht"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.fernkampfwaffe.munitionsgewicht" value=item.system.fernkampfwaffe.munitionsgewicht}}</div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.reichweiten"}}</span>
|
||||
<div class="colspan3 grid5">
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.reichweite1" value=item.system.fernkampfwaffe.reichweite1}}
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.reichweite2" value=item.system.fernkampfwaffe.reichweite2}}
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.reichweite3" value=item.system.fernkampfwaffe.reichweite3}}
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.reichweite4" value=item.system.fernkampfwaffe.reichweite4}}
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.reichweite5" value=item.system.fernkampfwaffe.reichweite5}}
|
||||
</div>
|
||||
|
||||
<span>{{localize "DSA41.bewaffnung.fernkampfwaffe.modifikator"}}</span>
|
||||
<div class="row colspan3 grid5">
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.modifikator1" value=item.system.fernkampfwaffe.modifikator1}}
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.modifikator2" value=item.system.fernkampfwaffe.modifikator2}}
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.modifikator3" value=item.system.fernkampfwaffe.modifikator3}}
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.modifikator4" value=item.system.fernkampfwaffe.modifikator4}}
|
||||
{{>editable-input type="number" name="system.fernkampfwaffe.modifikator5" value=item.system.fernkampfwaffe.modifikator5}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="item-sheet {{ cssClass }}" autocomplete="off">
|
||||
<div class="row">
|
||||
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" {{#if editable}}data-edit="img"{{/if}}>
|
||||
<div class="col">
|
||||
{{>editable-input type="text" name="name" value=item.name placeholder=(localize "DSA41.name")}}
|
||||
<div class="row gap">
|
||||
{{>editable-input type="number" name="system.gewicht" value=item.system.gewicht placeholder=(localize "DSA41.weight")}}
|
||||
{{>editable-input type="number" name="system.preis" value=item.system.preis placeholder=(localize "DSA41.price")}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="item-sheet {{ cssClass }}" autocomplete="off">
|
||||
<div class="row">
|
||||
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" {{#if editable}}data-edit="img"{{/if}}>
|
||||
<div class="col">
|
||||
<div class="grid3 gap">
|
||||
{{>editable-input type="text" name="name" value=item.name placeholder=(localize "DSA41.name")}}
|
||||
<div>
|
||||
<select name="system.steigern">
|
||||
<option value="A_Star" {{#if (eq item.system.steigern "A_Star")}}selected{{/if}}>{{localize "DSA41.kampftalent.steigern.A_Star"}}</option>
|
||||
<option value="A" {{#if (eq item.system.steigern "A") }}selected{{/if}}>{{localize "DSA41.kampftalent.steigern.A"}} </option>
|
||||
<option value="B" {{#if (eq item.system.steigern "B") }}selected{{/if}}>{{localize "DSA41.kampftalent.steigern.B"}} </option>
|
||||
<option value="C" {{#if (eq item.system.steigern "C") }}selected{{/if}}>{{localize "DSA41.kampftalent.steigern.C"}} </option>
|
||||
<option value="D" {{#if (eq item.system.steigern "D") }}selected{{/if}}>{{localize "DSA41.kampftalent.steigern.D"}} </option>
|
||||
<option value="E" {{#if (eq item.system.steigern "E") }}selected{{/if}}>{{localize "DSA41.kampftalent.steigern.E"}} </option>
|
||||
<option value="F" {{#if (eq item.system.steigern "F") }}selected{{/if}}>{{localize "DSA41.kampftalent.steigern.F"}} </option>
|
||||
<option value="G" {{#if (eq item.system.steigern "G") }}selected{{/if}}>{{localize "DSA41.kampftalent.steigern.G"}} </option>
|
||||
<option value="H" {{#if (eq item.system.steigern "H") }}selected{{/if}}>{{localize "DSA41.kampftalent.steigern.H"}} </option>
|
||||
</select>
|
||||
<div class="placeholder">{{localize "DSA41.kampftalent.label_steigern"}}</div>
|
||||
</div>
|
||||
{{>editable-input type="text" name="system.behinderung" value=item.system.behinderung placeholder=(localize "DSA41.talente.label_behinderung")}}
|
||||
</div>
|
||||
<div class="grid gap">
|
||||
<div>
|
||||
<select name="system.kategorie">
|
||||
<option value="waffenlos" {{#if (eq item.system.kategorie "waffenlos")}}selected{{/if}}>{{localize "DSA41.kampftalent.kategorie.waffenlos"}}</option>
|
||||
<option value="nahkampf" {{#if (eq item.system.kategorie "nahkampf") }}selected{{/if}}>{{localize "DSA41.kampftalent.kategorie.nahkampf"}} </option>
|
||||
<option value="fernkampf" {{#if (eq item.system.kategorie "fernkampf")}}selected{{/if}}>{{localize "DSA41.kampftalent.kategorie.fernkampf"}}</option>
|
||||
</select>
|
||||
<div class="placeholder">{{localize "DSA41.talente.label_kategorie"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<prose-mirror name="system.beschreibung" value="{{item.system.beschreibung}}" compact="true">
|
||||
</prose-mirror>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="item-sheet {{ cssClass }}" autocomplete="off">
|
||||
<div class="row">
|
||||
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" {{#if editable}}data-edit="img"{{/if}}>
|
||||
<div class="col">
|
||||
{{>editable-input type="text" name="name" value=item.name placeholder=(localize "DSA41.name")}}
|
||||
<div class="row gap">
|
||||
{{>editable-input type="number" name="system.gewicht" value=item.system.gewicht placeholder=(localize "DSA41.weight")}}
|
||||
{{>editable-input type="number" name="system.preis" value=item.system.preis placeholder=(localize "DSA41.price")}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab active">
|
||||
<span>{{localize "DSA41.ruestungen.kopf"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.kopf" value=item.system.kopf}}</div>
|
||||
<span>{{localize "DSA41.ruestungen.brust"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.brust" value=item.system.brust}}</div>
|
||||
|
||||
<span>{{localize "DSA41.ruestungen.ruecken"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.ruecken" value=item.system.ruecken}}</div>
|
||||
<span>{{localize "DSA41.ruestungen.bauch"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.bauch" value=item.system.bauch}}</div>
|
||||
|
||||
<span>{{localize "DSA41.ruestungen.linker_arm"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.linker_arm" value=item.system.linker_arm}}</div>
|
||||
<span>{{localize "DSA41.ruestungen.rechter_arm"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.rechter_arm" value=item.system.rechter_arm}}</div>
|
||||
|
||||
<span>{{localize "DSA41.ruestungen.linkes_bein"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.linkes_bein" value=item.system.linkes_bein}}</div>
|
||||
<span>{{localize "DSA41.ruestungen.rechtes_bein"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.rechtes_bein" value=item.system.rechtes_bein}}</div>
|
||||
|
||||
<span>{{localize "DSA41.ruestungen.gesamt_ruestungsschutz"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.gesamt_ruestungsschutz" value=item.system.gesamt_ruestungsschutz}}</div>
|
||||
<span>{{localize "DSA41.ruestungen.gesamt_behinderung"}}</span>
|
||||
<div>{{>editable-input type="number" name="system.gesamt_behinderung" value=item.system.gesamt_behinderung}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="item-sheet {{ cssClass }}" autocomplete="off">
|
||||
<div class="row">
|
||||
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" {{#if editable}}data-edit="img"{{/if}}>
|
||||
<div class="col">
|
||||
<div class="grid2 gap">
|
||||
{{>editable-input type="text" name="name" value=item.name placeholder=(localize "DSA41.name")}}
|
||||
<div>
|
||||
<select name="system.kategorie">
|
||||
<option value="allgemein" {{#if (eq item.system.kategorie "allgemein")}}selected{{/if}}>{{localize "DSA41.sonderfertigkeiten.kategorie.allgemein"}}</option>
|
||||
<option value="kampf" {{#if (eq item.system.kategorie "kampf") }}selected{{/if}}>{{localize "DSA41.sonderfertigkeiten.kategorie.kampf"}} </option>
|
||||
<option value="magisch" {{#if (eq item.system.kategorie "magisch") }}selected{{/if}}>{{localize "DSA41.sonderfertigkeiten.kategorie.magisch"}} </option>
|
||||
<option value="klerikal" {{#if (eq item.system.kategorie "klerikal") }}selected{{/if}}>{{localize "DSA41.sonderfertigkeiten.kategorie.klerikal"}} </option>
|
||||
</select>
|
||||
<div class="placeholder">{{localize "DSA41.sonderfertigkeiten.kategorie.label"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid2 gap">
|
||||
{{>editable-input type="number" name="system.kosten" value=item.system.kosten placeholder=(localize "DSA41.sonderfertigkeiten.kosten")}}
|
||||
{{>editable-input type="number" name="system.verbreitung" value=item.system.verbreitung placeholder=(localize "DSA41.sonderfertigkeiten.verbreitung")}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<prose-mirror name="system.beschreibung" value="{{item.system.beschreibung}}" compact="true">
|
||||
|
||||
</prose-mirror>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
<div class="item-sheet {{ cssClass }}" autocomplete="off">
|
||||
<div class="row">
|
||||
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" {{#if editable}}data-edit="img"{{/if}}>
|
||||
<div class="col">
|
||||
<div class="grid3 gap">
|
||||
{{>editable-input type="text" name="name" value=item.name placeholder=(localize "DSA41.name")}}
|
||||
<div>
|
||||
<select name="system.kategorie">
|
||||
<option value="koerperliche" {{#if (eq item.system.kategorie "koerperliche") }}selected{{/if}}>{{localize "DSA41.talente.koerperliche.label"}} </option>
|
||||
<option value="gesellschaftliche" {{#if (eq item.system.kategorie "gesellschaftliche")}}selected{{/if}}>{{localize "DSA41.talente.gesellschaftliche.label"}}</option>
|
||||
<option value="natur" {{#if (eq item.system.kategorie "natur") }}selected{{/if}}>{{localize "DSA41.talente.natur.label"}} </option>
|
||||
<option value="wissens" {{#if (eq item.system.kategorie "wissens") }}selected{{/if}}>{{localize "DSA41.talente.wissens.label"}} </option>
|
||||
<option value="handwerks" {{#if (eq item.system.kategorie "handwerks") }}selected{{/if}}>{{localize "DSA41.talente.handwerks.label"}} </option>
|
||||
</select>
|
||||
<div class="placeholder">{{localize "DSA41.talente.label_kategorie"}}</div>
|
||||
</div>
|
||||
{{>editable-input type="text" name="system.behinderung" value=item.system.behinderung placeholder=(localize "DSA41.talente.label_behinderung")}}
|
||||
</div>
|
||||
<div class="grid3 gap">
|
||||
<select name="system.attribute1">
|
||||
<option value="courage" {{#if (eq item.system.attribute1 "courage") }}selected{{/if}}>{{localize "DSA41.attributes.long.courage"}} </option>
|
||||
<option value="cleverness" {{#if (eq item.system.attribute1 "cleverness") }}selected{{/if}}>{{localize "DSA41.attributes.long.cleverness"}} </option>
|
||||
<option value="intuition" {{#if (eq item.system.attribute1 "intuition") }}selected{{/if}}>{{localize "DSA41.attributes.long.intuition"}} </option>
|
||||
<option value="charisma" {{#if (eq item.system.attribute1 "charisma") }}selected{{/if}}>{{localize "DSA41.attributes.long.charisma"}} </option>
|
||||
<option value="dexterity" {{#if (eq item.system.attribute1 "dexterity") }}selected{{/if}}>{{localize "DSA41.attributes.long.dexterity"}} </option>
|
||||
<option value="agility" {{#if (eq item.system.attribute1 "agility") }}selected{{/if}}>{{localize "DSA41.attributes.long.agility"}} </option>
|
||||
<option value="constitution" {{#if (eq item.system.attribute1 "constitution")}}selected{{/if}}>{{localize "DSA41.attributes.long.constitution"}}</option>
|
||||
<option value="strength" {{#if (eq item.system.attribute1 "strength") }}selected{{/if}}>{{localize "DSA41.attributes.long.strength"}} </option>
|
||||
</select>
|
||||
|
||||
<select name="system.attribute2">
|
||||
<option value="courage" {{#if (eq item.system.attribute2 "courage") }}selected{{/if}}>{{localize "DSA41.attributes.long.courage"}} </option>
|
||||
<option value="cleverness" {{#if (eq item.system.attribute2 "cleverness") }}selected{{/if}}>{{localize "DSA41.attributes.long.cleverness"}} </option>
|
||||
<option value="intuition" {{#if (eq item.system.attribute2 "intuition") }}selected{{/if}}>{{localize "DSA41.attributes.long.intuition"}} </option>
|
||||
<option value="charisma" {{#if (eq item.system.attribute2 "charisma") }}selected{{/if}}>{{localize "DSA41.attributes.long.charisma"}} </option>
|
||||
<option value="dexterity" {{#if (eq item.system.attribute2 "dexterity") }}selected{{/if}}>{{localize "DSA41.attributes.long.dexterity"}} </option>
|
||||
<option value="agility" {{#if (eq item.system.attribute2 "agility") }}selected{{/if}}>{{localize "DSA41.attributes.long.agility"}} </option>
|
||||
<option value="constitution" {{#if (eq item.system.attribute2 "constitution")}}selected{{/if}}>{{localize "DSA41.attributes.long.constitution"}}</option>
|
||||
<option value="strength" {{#if (eq item.system.attribute2 "strength") }}selected{{/if}}>{{localize "DSA41.attributes.long.strength"}} </option>
|
||||
</select>
|
||||
|
||||
<select name="system.attribute3">
|
||||
<option value="courage" {{#if (eq item.system.attribute3 "courage") }}selected{{/if}}>{{localize "DSA41.attributes.long.courage"}} </option>
|
||||
<option value="cleverness" {{#if (eq item.system.attribute3 "cleverness") }}selected{{/if}}>{{localize "DSA41.attributes.long.cleverness"}} </option>
|
||||
<option value="intuition" {{#if (eq item.system.attribute3 "intuition") }}selected{{/if}}>{{localize "DSA41.attributes.long.intuition"}} </option>
|
||||
<option value="charisma" {{#if (eq item.system.attribute3 "charisma") }}selected{{/if}}>{{localize "DSA41.attributes.long.charisma"}} </option>
|
||||
<option value="dexterity" {{#if (eq item.system.attribute3 "dexterity") }}selected{{/if}}>{{localize "DSA41.attributes.long.dexterity"}} </option>
|
||||
<option value="agility" {{#if (eq item.system.attribute3 "agility") }}selected{{/if}}>{{localize "DSA41.attributes.long.agility"}} </option>
|
||||
<option value="constitution" {{#if (eq item.system.attribute3 "constitution")}}selected{{/if}}>{{localize "DSA41.attributes.long.constitution"}}</option>
|
||||
<option value="strength" {{#if (eq item.system.attribute3 "strength") }}selected{{/if}}>{{localize "DSA41.attributes.long.strength"}} </option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="item-sheet {{ cssClass }}" autocomplete="off">
|
||||
<div class="row">
|
||||
<img class="item-image" src="{{ item.img }}" title="{{ item.name }}" {{#if editable}}data-edit="img"{{/if}}>
|
||||
<div class="col">
|
||||
<div class="grid2 gap">
|
||||
{{>editable-input type="text" name="name" value=item.name placeholder=(localize "DSA41.name")}}
|
||||
{{>editable-input type="number" name="system.kosten" value=item.system.kosten placeholder=(localize "DSA41.vornachteil.kosten")}}
|
||||
</div>
|
||||
<div>
|
||||
<select name="system.kategorie">
|
||||
<option value="vorteil" {{#if (eq item.system.kategorie "vorteil") }}selected{{/if}}>{{localize "DSA41.vornachteil.kategorie.vorteil"}} </option>
|
||||
<option value="nachteil" {{#if (eq item.system.kategorie "nachteil")}}selected{{/if}}>{{localize "DSA41.vornachteil.kategorie.nachteil"}} </option>
|
||||
</select>
|
||||
<div class="placeholder">{{localize "DSA41.vornachteil.kategorie.label"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<prose-mirror name="system.beschreibung" value="{{item.system.beschreibung}}" compact="true">
|
||||
</prose-mirror>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="dsa41-tooltip">
|
||||
<ruby>{{item.system.fernkampfwaffe.basis}}<rt>{{localize "DSA41.bewaffnung.fernkampfwaffe.basis"}}</rt></ruby>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+467
-5
@@ -1,9 +1,131 @@
|
||||
{
|
||||
"TYPES": {
|
||||
"Actor": {
|
||||
"Player": "Spieler"
|
||||
},
|
||||
|
||||
"Item": {
|
||||
"Gegenstand": "Gegenstand",
|
||||
"Ruestung": "Rüstung",
|
||||
"Bewaffnung": "Bewaffnung",
|
||||
"Talent": "Talent",
|
||||
"Kampftalent": "Kampftalent",
|
||||
"Sonderfertigkeit": "Sonderfertigkeit",
|
||||
"VorNachteil": "Vor-/Nachteil"
|
||||
}
|
||||
},
|
||||
|
||||
"DSA41": {
|
||||
"name": "Name",
|
||||
"race": "Rasse",
|
||||
"culture": "Kultur",
|
||||
"profession": "Profession",
|
||||
"name": "Name",
|
||||
"race": "Rasse",
|
||||
"culture": "Kultur",
|
||||
"profession": "Profession",
|
||||
"sozialstatus": "Sozialstatus",
|
||||
"weight": "Gewicht",
|
||||
"price": "Preis",
|
||||
"abenteuerpunkte": "Abenteuerpunkte",
|
||||
|
||||
"list_empty": "Keine",
|
||||
|
||||
"currency": {
|
||||
"dukaten": "Dukaten",
|
||||
"silbertaler": "Silbertaler",
|
||||
"heller": "Heller",
|
||||
"kreuzer": "Kreuzer"
|
||||
},
|
||||
|
||||
"chat": {
|
||||
"result": "Ergebnis",
|
||||
"success": "Geschafft",
|
||||
"failure": "Fehlgeschlagen",
|
||||
|
||||
"attribute": "Eigenshaft",
|
||||
"value": "Wert",
|
||||
"roll": "Wurf",
|
||||
|
||||
"talentwert_short": "TaW",
|
||||
|
||||
"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",
|
||||
|
||||
"attacke": "Attacke",
|
||||
"parade": "Parade",
|
||||
"trefferpunkte": "Trefferpunkte",
|
||||
|
||||
"fernkampf-attacke": "Attacke",
|
||||
"fernkampf-trefferpunkte": "Trefferpunkte"
|
||||
},
|
||||
|
||||
"character": {
|
||||
"eigenschaften": "Eigenschaften",
|
||||
"talente": "Talente",
|
||||
"inventar": "Inventar",
|
||||
"kampf": "Kampf"
|
||||
},
|
||||
|
||||
"attributes": {
|
||||
"label": "Eigenschaften",
|
||||
@@ -32,6 +154,346 @@
|
||||
"constitution": "KO",
|
||||
"strength": "KK"
|
||||
}
|
||||
},
|
||||
|
||||
"kampftalent": {
|
||||
"label": "Kampftalent",
|
||||
"label_kategorie": "Kategorie",
|
||||
"label_steigern": "Steigern",
|
||||
|
||||
"kategorie":{
|
||||
"waffenlos": "Waffenlos",
|
||||
"nahkampf": "Bewaffneter Nahkampf",
|
||||
"fernkampf": "Fernkampf"
|
||||
},
|
||||
|
||||
"steigern": {
|
||||
"A_Star": "A*",
|
||||
"A": "A",
|
||||
"B": "B",
|
||||
"C": "C",
|
||||
"D": "D",
|
||||
"E": "E",
|
||||
"F": "F",
|
||||
"G": "G",
|
||||
"H": "H"
|
||||
}
|
||||
},
|
||||
|
||||
"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",
|
||||
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+511
-82
@@ -1,98 +1,527 @@
|
||||
.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 application content to be scrolled */
|
||||
& .window-content {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
& [data-action] {
|
||||
cursor: pointer;
|
||||
|
||||
&:not(button):hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
& .small {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.editable-input {
|
||||
flex: 1;
|
||||
padding: 0px 3px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
& .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; }
|
||||
|
||||
& .editable-input {
|
||||
flex: 1;
|
||||
|
||||
& input {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.editable-number {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
& .placeholder {
|
||||
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;
|
||||
}
|
||||
|
||||
&.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;
|
||||
|
||||
& > * {
|
||||
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%);
|
||||
}
|
||||
|
||||
& .editable-input, & span {
|
||||
display: inline-block;
|
||||
width: 4ch;
|
||||
|
||||
& input {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.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);
|
||||
|
||||
& .editable-input {
|
||||
display: inline-block;
|
||||
width: 7ch;
|
||||
|
||||
& input {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
& .Abenteuerpunkte {
|
||||
width: min-content;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
|
||||
& .editable-input {
|
||||
display: inline-block;
|
||||
width: 5ch;
|
||||
|
||||
& input {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .tabs {
|
||||
padding: .5rem;
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
border-top: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
|
||||
& > * {
|
||||
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="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="ActorSheet"] {
|
||||
& .tab {
|
||||
&[data-tab="tab1"] {
|
||||
& .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="tab2"] {
|
||||
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="tab3"] {
|
||||
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="tab4"] {
|
||||
& .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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .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;
|
||||
}
|
||||
|
||||
.item-sheet header {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
#tooltip.DSA41 {
|
||||
max-width: 650px;
|
||||
text-align: left;
|
||||
|
||||
.item-sheet header img {
|
||||
flex: 0 0 64px;
|
||||
height: 64px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.item-sheet header input,
|
||||
.item-sheet header div {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
margin: 8px;
|
||||
font-size: 2em;
|
||||
& h4 {
|
||||
font-size: large;
|
||||
}
|
||||
}
|
||||
+1120
-28
File diff suppressed because it is too large
Load Diff
+56
-5
@@ -5,19 +5,70 @@
|
||||
"esmodules": ["src/main.mjs"],
|
||||
"styles": ["src/main.css"],
|
||||
|
||||
"documentTypes": {
|
||||
"Actor": {
|
||||
"Player": {}
|
||||
},
|
||||
"Item": {
|
||||
"Gegenstand": {},
|
||||
"Ruestung": {},
|
||||
"Bewaffnung": {},
|
||||
|
||||
"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"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"Actor": {
|
||||
"types": [
|
||||
"Player"
|
||||
]
|
||||
},
|
||||
"Item": {
|
||||
"types": [
|
||||
"Generic Item",
|
||||
"Melee Weapon",
|
||||
"Ranged Weapon",
|
||||
"Armor",
|
||||
"Shield"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 = 5, .preis = 0 } } },
|
||||
.{ .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 = 3, .preis = 0 } } },
|
||||
.{ .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 = 3, .preis = 0 } } },
|
||||
.{ .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 = 2, .preis = 0 } } },
|
||||
.{ .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 = 2, .preis = 0 } } },
|
||||
.{ .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 = 2, .preis = 0 } } },
|
||||
.{ .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 = 2, .preis = 0 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 3, .preis = 40 } } },
|
||||
.{ .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 = 3.5, .preis = 65 } } },
|
||||
.{ .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 = 2.5, .preis = 50 } } },
|
||||
.{ .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 = 4, .preis = 80 } } },
|
||||
.{ .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 = 0.5, .preis = 5 } } },
|
||||
.{ .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 = 2.5, .preis = 25 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 1, .preis = 15 } } },
|
||||
.{ .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 = 1, .preis = 25 } } },
|
||||
.{ .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 = 2, .preis = 50 } } },
|
||||
.{ .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 = 3.5, .preis = 125 } } },
|
||||
.{ .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 = 4, .preis = 60 } } },
|
||||
.{ .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 = 4.5, .preis = 80 } } },
|
||||
.{ .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 = 1.5, .preis = 20 } } },
|
||||
.{ .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 = 1.75, .preis = 30 } } },
|
||||
.{ .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 = 3, .preis = 40 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 6, .preis = 1500 } } },
|
||||
.{ .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 = 7, .preis = 1200 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 9, .preis = 180 } } },
|
||||
.{ .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 = 6, .preis = 350 } } },
|
||||
.{ .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 = 6, .preis = 500 } } },
|
||||
.{ .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 = 7, .preis = 600 } } },
|
||||
.{ .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 = 8, .preis = 200 } } },
|
||||
.{ .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 = 1.5, .preis = 100 } } },
|
||||
.{ .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 = 3.5, .preis = 80 } } },
|
||||
.{ .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 = 4, .preis = 100 } } },
|
||||
.{ .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 = 6.5, .preis = 150 } } },
|
||||
.{ .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 = 10, .preis = 180 } } },
|
||||
.{ .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 = 12, .preis = 500 } } },
|
||||
.{ .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 = 2.5, .preis = 60 } } },
|
||||
.{ .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 = 5, .preis = 100 } } },
|
||||
.{ .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 = 5, .preis = 100 } } },
|
||||
.{ .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 = 7, .preis = 550 } } },
|
||||
.{ .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 = 12, .preis = 1000 } } },
|
||||
.{ .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 = 18, .preis = 1200 } } },
|
||||
.{ .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 = 10, .preis = 1000 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 8, .preis = 0 } } },
|
||||
.{ .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 = 1.5, .preis = 25 } } },
|
||||
.{ .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 = 1.5, .preis = 35 } } },
|
||||
.{ .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 = 3, .preis = 60 } } },
|
||||
.{ .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 = 1, .preis = 45 } } },
|
||||
.{ .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 = 3, .preis = 35 } } },
|
||||
.{ .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 = 3, .preis = 50 } } },
|
||||
.{ .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 = 2, .preis = 90 } } },
|
||||
.{ .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 = 6, .preis = 250 } } },
|
||||
.{ .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 = 2, .preis = 50 } } },
|
||||
.{ .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 = 0.5, .preis = 25 } } },
|
||||
.{ .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 = 3, .preis = 80 } } },
|
||||
.{ .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 = 14, .preis = 750 } } },
|
||||
.{ .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 = 30, .preis = 2500 } } },
|
||||
.{ .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 = 4, .preis = 180 } } },
|
||||
.{ .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 = 17, .preis = 1000 } } },
|
||||
.{ .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 = 4, .preis = 110 } } },
|
||||
.{ .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 = 7.5, .preis = 500 } } },
|
||||
.{ .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 = 7.5, .preis = 250 } } },
|
||||
.{ .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 = 4, .preis = 75 } } },
|
||||
.{ .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 = 6, .preis = 150 } } },
|
||||
.{ .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 = 1.5, .preis = 120 } } },
|
||||
.{ .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 = 1, .preis = 120 } } },
|
||||
.{ .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 = 3, .preis = 150 } } },
|
||||
.{ .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 = 3, .preis = 200 } } },
|
||||
.{ .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 = 4, .preis = 60 } } },
|
||||
.{ .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 = 4, .preis = 100 } } },
|
||||
.{ .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 = 3.5, .preis = 70 } } },
|
||||
.{ .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 = 1.5, .preis = 30 } } },
|
||||
.{ .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 = 4.5, .preis = 80 } } },
|
||||
}}},
|
||||
}};
|
||||
|
||||
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 = 115, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 250 } } },
|
||||
.{ .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 = 110, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 200 } } },
|
||||
.{ .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 = 100, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 70, .preis = 500 } } },
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = 130, .preis = 0 } } },
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 400 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 30, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 25, .preis = 70 } } },
|
||||
.{ .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 = 40, .zweihaendig = false, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = 30, .preis = 40 } } },
|
||||
.{ .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 = 30, .zweihaendig = false, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = 20, .preis = 20 } } },
|
||||
.{ .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 = 40, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 40, .preis = 120 } } },
|
||||
.{ .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 = 40, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 40, .preis = 60 } } },
|
||||
.{ .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 = 60, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 50, .preis = 90 } } },
|
||||
.{ .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 = 30, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 15, .preis = 50 } } },
|
||||
.{ .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 = 50, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 40, .preis = 80 } } },
|
||||
.{ .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 = 40, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 30, .preis = 45 } } },
|
||||
.{ .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 = 30, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 30, .preis = 90 } } },
|
||||
.{ .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 = 25, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 20, .preis = 200 } } },
|
||||
.{ .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 = 25, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 10, .preis = 10 } } },
|
||||
.{ .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 = 35, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 35, .preis = 150 } } },
|
||||
.{ .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 = 45, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 40, .preis = 60 } } },
|
||||
.{ .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 = 35, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 30, .preis = 40 } } },
|
||||
.{ .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 = 30, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 30, .preis = 0 } } },
|
||||
.{ .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 = 45, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 35, .preis = 60 } } },
|
||||
.{ .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 = 25, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = 20, .preis = 30 } } },
|
||||
.{ .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 = 20, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = 10, .preis = 15 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 40, .preis = 150 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 30, .preis = 180 } } },
|
||||
.{ .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 = 40, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 30, .preis = 45 } } },
|
||||
.{ .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 = 75, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 30, .preis = 150 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 120 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 45, .preis = 120 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 70, .preis = 200 } } },
|
||||
.{ .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 = 80, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 35, .preis = 180 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 50, .preis = 250 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 80, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 180 } } },
|
||||
.{ .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 = 50, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 70, .preis = 20 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 100 } } },
|
||||
.{ .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 = 100, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 130, .preis = 90 } } },
|
||||
.{ .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 = 50, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 30, .preis = 0.5 } } },
|
||||
.{ .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 = 30, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 60, .preis = 20 } } },
|
||||
.{ .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 = 50, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 90, .preis = 40 } } },
|
||||
.{ .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 = 80, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 15 } } },
|
||||
.{ .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 = 100, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 110, .preis = 0 } } },
|
||||
.{ .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 = 80, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 60, .preis = 1 } } },
|
||||
.{ .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 = 40, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 50, .preis = 250 } } },
|
||||
.{ .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 = 50, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 95, .preis = 120 } } },
|
||||
.{ .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 = 100, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 90 } } },
|
||||
.{ .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 = 100, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 110, .preis = 75 } } },
|
||||
.{ .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 = 110, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 90, .preis = 130 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = 150, .preis = 0 } } },
|
||||
.{ .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 = 50, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 30, .preis = 25 } } },
|
||||
.{ .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 = 70, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 90, .preis = 50 } } },
|
||||
.{ .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 = 70, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = 90, .preis = 0 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 50 } } },
|
||||
.{ .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 = 75, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 50 } } },
|
||||
.{ .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 = 40, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 40, .preis = 0 } } },
|
||||
.{ .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 = 40, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = 50, .preis = 35 } } },
|
||||
.{ .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 = 40, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = 35, .preis = 18 } } },
|
||||
.{ .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 = 60, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 100 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 200, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 45 } } },
|
||||
.{ .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 = 250, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 70 } } },
|
||||
.{ .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 = 200, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 150, .preis = 75 } } },
|
||||
.{ .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 = 180, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = 140, .preis = 200 } } },
|
||||
.{ .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 = 180, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 160, .preis = 160 } } },
|
||||
.{ .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 = 175, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 180, .preis = 300 } } },
|
||||
.{ .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 = 200, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 150, .preis = 80 } } },
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 90, .preis = 120 } } },
|
||||
.{ .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 = 160, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 100, .preis = 30 } } },
|
||||
.{ .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 = 180, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 40 } } },
|
||||
.{ .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 = 150, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 150, .preis = 150 } } },
|
||||
.{ .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 = 180, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 120 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 180 } } },
|
||||
.{ .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 = 120, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 120 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 100, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 30, .preis = 15 } } },
|
||||
.{ .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 = 100, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 250, .preis = 150 } } },
|
||||
.{ .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 = 100, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 140, .preis = 100 } } },
|
||||
.{ .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 = 120, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 60 } } },
|
||||
.{ .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 = 110, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 300, .preis = 250 } } },
|
||||
.{ .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 = 120, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 240, .preis = 180 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 250, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 60, .preis = 25 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 100, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 75, .preis = 180 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 120 } } },
|
||||
.{ .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 = 75, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 70, .preis = 50 } } },
|
||||
.{ .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 = 50, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 90, .preis = 40 } } },
|
||||
.{ .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 = 80, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 90, .preis = 130 } } },
|
||||
.{ .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 = 50, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 40, .preis = 80 } } },
|
||||
.{ .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 = 80, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 70, .preis = 160 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 70, .preis = 200 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 60, .preis = 100 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 250 } } },
|
||||
.{ .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 = 45, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 35, .preis = 60 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 50, .preis = 250 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 100, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 75, .preis = 180 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 120 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 200 } } },
|
||||
.{ .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 = 110, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 200 } } },
|
||||
.{ .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 = 85, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 120 } } },
|
||||
.{ .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 = 50, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 40, .preis = 80 } } },
|
||||
.{ .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 = 80, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 70, .preis = 160 } } },
|
||||
.{ .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 = 95, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 180 } } },
|
||||
.{ .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 = 100, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 70, .preis = 500 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 45, .preis = 120 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 70, .preis = 200 } } },
|
||||
.{ .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 60, .preis = 100 } } },
|
||||
// .{ .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 = 80, .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 = 90, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 50, .preis = 250 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 400, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 400, .preis = 0 } } },
|
||||
.{ .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 = 140, .zweihaendig = false, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 90, .preis = 50 } } },
|
||||
.{ .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 = 200, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 120 } } },
|
||||
.{ .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 = 120, .zweihaendig = false, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = 90, .preis = 80 } } },
|
||||
.{ .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 = 150, .zweihaendig = true, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = 60, .preis = 10 } } },
|
||||
.{ .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 = 200, .zweihaendig = true, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 80 } } },
|
||||
.{ .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 = 180, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = 140, .preis = 200 } } },
|
||||
.{ .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 = 300, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 150, .preis = 120 } } },
|
||||
.{ .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 = 200, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 150, .preis = 80 } } },
|
||||
.{ .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 = 350, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 180, .preis = 50 } } },
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 90, .preis = 120 } } },
|
||||
.{ .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 = 190, .zweihaendig = true, .werfbar = true, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 30 } } },
|
||||
.{ .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 = 200, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 150, .preis = 100 } } },
|
||||
// .{ .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 = 300, .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 = 100, .zweihaendig = false, .werfbar = true, .improvisiert = true, .priviligiert = false }, .gewicht = 80, .preis = 30 } } },
|
||||
.{ .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 = 180, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 120 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 150, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 40 } } },
|
||||
.{ .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 = 150, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = 90, .preis = 0 } } },
|
||||
.{ .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 = 150, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = 150, .preis = 0 } } },
|
||||
.{ .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 = 140, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 80, .preis = 200 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 150, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 100, .preis = 15 } } },
|
||||
.{ .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 = 150, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 50 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 120, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 250, .preis = 150 } } },
|
||||
.{ .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 = 150, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 90, .preis = 0 } } },
|
||||
.{ .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 = 120, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 150, .preis = 300 } } },
|
||||
.{ .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 = 120, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 180, .preis = 120 } } },
|
||||
.{ .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 = 110, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 160, .preis = 80 } } },
|
||||
.{ .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 = 100, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 180, .preis = 120 } } },
|
||||
.{ .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 = 180, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 160, .preis = 160 } } },
|
||||
.{ .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 = 100, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 110, .preis = 75 } } },
|
||||
.{ .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 = 175, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 180, .preis = 300 } } },
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 90, .preis = 120 } } },
|
||||
.{ .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 = 100, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 200, .preis = 20 } } },
|
||||
.{ .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 = 90, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = false }, .gewicht = 250, .preis = 30 } } },
|
||||
.{ .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 = 150, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 150, .preis = 150 } } },
|
||||
.{ .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 = 120, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 150 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = 200, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 220, .preis = 350 } } },
|
||||
.{ .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 = 115, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 250 } } },
|
||||
.{ .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 = 180, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 160, .preis = 400 } } },
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 150, .preis = 250 } } },
|
||||
.{ .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 = 140, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 160, .preis = 350 } } },
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = true, .priviligiert = true }, .gewicht = 200, .preis = 0 } } },
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = 130, .preis = 0 } } },
|
||||
.{ .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 = 130, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 100, .preis = 400 } } },
|
||||
.{ .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 = 155, .zweihaendig = true, .werfbar = false, .improvisiert = false, .priviligiert = true }, .gewicht = 160, .preis = 250 } } },
|
||||
}}},
|
||||
|
||||
.{ .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 = -, .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 = -, .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 = 20, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 200, .preis = 350 } } },
|
||||
.{ .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 = 30, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 200, .preis = 390 } } },
|
||||
.{ .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 = 20, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 200, .preis = 390 } } },
|
||||
.{ .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 = 20, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 200, .preis = 410 } } },
|
||||
.{ .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 = 0, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 70, .preis = 250 } } },
|
||||
// .{ .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 = -, .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 = 0, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 35, .preis = 180 } } },
|
||||
.{ .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 = 20, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 220, .preis = 140 } } },
|
||||
.{ .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 = 20, .zweihaendig = false, .werfbar = false, .improvisiert = false, .priviligiert = false }, .gewicht = 120, .preis = 80 } } },
|
||||
}}},
|
||||
}};
|
||||
|
||||
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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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/).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
[](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.
|
||||
@@ -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_
|
||||
@@ -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"
|
||||
@@ -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
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_), ¤t));
|
||||
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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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/table_cache.h"
|
||||
|
||||
#include "db/filename.h"
|
||||
#include "leveldb/env.h"
|
||||
#include "leveldb/table.h"
|
||||
#include "util/coding.h"
|
||||
|
||||
namespace leveldb {
|
||||
|
||||
struct TableAndFile {
|
||||
RandomAccessFile* file;
|
||||
Table* table;
|
||||
};
|
||||
|
||||
static void DeleteEntry(const Slice& key, void* value) {
|
||||
TableAndFile* tf = reinterpret_cast<TableAndFile*>(value);
|
||||
delete tf->table;
|
||||
delete tf->file;
|
||||
delete tf;
|
||||
}
|
||||
|
||||
static void UnrefEntry(void* arg1, void* arg2) {
|
||||
Cache* cache = reinterpret_cast<Cache*>(arg1);
|
||||
Cache::Handle* h = reinterpret_cast<Cache::Handle*>(arg2);
|
||||
cache->Release(h);
|
||||
}
|
||||
|
||||
TableCache::TableCache(const std::string& dbname, const Options& options,
|
||||
int entries)
|
||||
: env_(options.env),
|
||||
dbname_(dbname),
|
||||
options_(options),
|
||||
cache_(NewLRUCache(entries)) {}
|
||||
|
||||
TableCache::~TableCache() { delete cache_; }
|
||||
|
||||
Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,
|
||||
Cache::Handle** handle) {
|
||||
Status s;
|
||||
char buf[sizeof(file_number)];
|
||||
EncodeFixed64(buf, file_number);
|
||||
Slice key(buf, sizeof(buf));
|
||||
*handle = cache_->Lookup(key);
|
||||
if (*handle == nullptr) {
|
||||
std::string fname = TableFileName(dbname_, file_number);
|
||||
RandomAccessFile* file = nullptr;
|
||||
Table* table = nullptr;
|
||||
s = env_->NewRandomAccessFile(fname, &file);
|
||||
if (!s.ok()) {
|
||||
std::string old_fname = SSTTableFileName(dbname_, file_number);
|
||||
if (env_->NewRandomAccessFile(old_fname, &file).ok()) {
|
||||
s = Status::OK();
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = Table::Open(options_, file, file_size, &table);
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
assert(table == nullptr);
|
||||
delete file;
|
||||
// We do not cache error results so that if the error is transient,
|
||||
// or somebody repairs the file, we recover automatically.
|
||||
} else {
|
||||
TableAndFile* tf = new TableAndFile;
|
||||
tf->file = file;
|
||||
tf->table = table;
|
||||
*handle = cache_->Insert(key, tf, 1, &DeleteEntry);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Iterator* TableCache::NewIterator(const ReadOptions& options,
|
||||
uint64_t file_number, uint64_t file_size,
|
||||
Table** tableptr) {
|
||||
if (tableptr != nullptr) {
|
||||
*tableptr = nullptr;
|
||||
}
|
||||
|
||||
Cache::Handle* handle = nullptr;
|
||||
Status s = FindTable(file_number, file_size, &handle);
|
||||
if (!s.ok()) {
|
||||
return NewErrorIterator(s);
|
||||
}
|
||||
|
||||
Table* table = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
|
||||
Iterator* result = table->NewIterator(options);
|
||||
result->RegisterCleanup(&UnrefEntry, cache_, handle);
|
||||
if (tableptr != nullptr) {
|
||||
*tableptr = table;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Status TableCache::Get(const ReadOptions& options, uint64_t file_number,
|
||||
uint64_t file_size, const Slice& k, void* arg,
|
||||
void (*handle_result)(void*, const Slice&,
|
||||
const Slice&)) {
|
||||
Cache::Handle* handle = nullptr;
|
||||
Status s = FindTable(file_number, file_size, &handle);
|
||||
if (s.ok()) {
|
||||
Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
|
||||
s = t->InternalGet(options, k, arg, handle_result);
|
||||
cache_->Release(handle);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void TableCache::Evict(uint64_t file_number) {
|
||||
char buf[sizeof(file_number)];
|
||||
EncodeFixed64(buf, file_number);
|
||||
cache_->Erase(Slice(buf, sizeof(buf)));
|
||||
}
|
||||
|
||||
} // namespace leveldb
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
//
|
||||
// Thread-safe (provides internal synchronization)
|
||||
|
||||
#ifndef STORAGE_LEVELDB_DB_TABLE_CACHE_H_
|
||||
#define STORAGE_LEVELDB_DB_TABLE_CACHE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "db/dbformat.h"
|
||||
#include "leveldb/cache.h"
|
||||
#include "leveldb/table.h"
|
||||
#include "port/port.h"
|
||||
|
||||
namespace leveldb {
|
||||
|
||||
class Env;
|
||||
|
||||
class TableCache {
|
||||
public:
|
||||
TableCache(const std::string& dbname, const Options& options, int entries);
|
||||
|
||||
TableCache(const TableCache&) = delete;
|
||||
TableCache& operator=(const TableCache&) = delete;
|
||||
|
||||
~TableCache();
|
||||
|
||||
// Return an iterator for the specified file number (the corresponding
|
||||
// file length must be exactly "file_size" bytes). If "tableptr" is
|
||||
// non-null, also sets "*tableptr" to point to the Table object
|
||||
// underlying the returned iterator, or to nullptr if no Table object
|
||||
// underlies the returned iterator. The returned "*tableptr" object is owned
|
||||
// by the cache and should not be deleted, and is valid for as long as the
|
||||
// returned iterator is live.
|
||||
Iterator* NewIterator(const ReadOptions& options, uint64_t file_number,
|
||||
uint64_t file_size, Table** tableptr = nullptr);
|
||||
|
||||
// If a seek to internal key "k" in specified file finds an entry,
|
||||
// call (*handle_result)(arg, found_key, found_value).
|
||||
Status Get(const ReadOptions& options, uint64_t file_number,
|
||||
uint64_t file_size, const Slice& k, void* arg,
|
||||
void (*handle_result)(void*, const Slice&, const Slice&));
|
||||
|
||||
// Evict any entry for the specified file number
|
||||
void Evict(uint64_t file_number);
|
||||
|
||||
private:
|
||||
Status FindTable(uint64_t file_number, uint64_t file_size, Cache::Handle**);
|
||||
|
||||
Env* const env_;
|
||||
const std::string dbname_;
|
||||
const Options& options_;
|
||||
Cache* cache_;
|
||||
};
|
||||
|
||||
} // namespace leveldb
|
||||
|
||||
#endif // STORAGE_LEVELDB_DB_TABLE_CACHE_H_
|
||||
@@ -0,0 +1,258 @@
|
||||
// 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/version_edit.h"
|
||||
|
||||
#include "db/version_set.h"
|
||||
#include "util/coding.h"
|
||||
|
||||
namespace leveldb {
|
||||
|
||||
// Tag numbers for serialized VersionEdit. These numbers are written to
|
||||
// disk and should not be changed.
|
||||
enum Tag {
|
||||
kComparator = 1,
|
||||
kLogNumber = 2,
|
||||
kNextFileNumber = 3,
|
||||
kLastSequence = 4,
|
||||
kCompactPointer = 5,
|
||||
kDeletedFile = 6,
|
||||
kNewFile = 7,
|
||||
// 8 was used for large value refs
|
||||
kPrevLogNumber = 9
|
||||
};
|
||||
|
||||
void VersionEdit::Clear() {
|
||||
comparator_.clear();
|
||||
log_number_ = 0;
|
||||
prev_log_number_ = 0;
|
||||
last_sequence_ = 0;
|
||||
next_file_number_ = 0;
|
||||
has_comparator_ = false;
|
||||
has_log_number_ = false;
|
||||
has_prev_log_number_ = false;
|
||||
has_next_file_number_ = false;
|
||||
has_last_sequence_ = false;
|
||||
compact_pointers_.clear();
|
||||
deleted_files_.clear();
|
||||
new_files_.clear();
|
||||
}
|
||||
|
||||
void VersionEdit::EncodeTo(std::string* dst) const {
|
||||
if (has_comparator_) {
|
||||
PutVarint32(dst, kComparator);
|
||||
PutLengthPrefixedSlice(dst, comparator_);
|
||||
}
|
||||
if (has_log_number_) {
|
||||
PutVarint32(dst, kLogNumber);
|
||||
PutVarint64(dst, log_number_);
|
||||
}
|
||||
if (has_prev_log_number_) {
|
||||
PutVarint32(dst, kPrevLogNumber);
|
||||
PutVarint64(dst, prev_log_number_);
|
||||
}
|
||||
if (has_next_file_number_) {
|
||||
PutVarint32(dst, kNextFileNumber);
|
||||
PutVarint64(dst, next_file_number_);
|
||||
}
|
||||
if (has_last_sequence_) {
|
||||
PutVarint32(dst, kLastSequence);
|
||||
PutVarint64(dst, last_sequence_);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < compact_pointers_.size(); i++) {
|
||||
PutVarint32(dst, kCompactPointer);
|
||||
PutVarint32(dst, compact_pointers_[i].first); // level
|
||||
PutLengthPrefixedSlice(dst, compact_pointers_[i].second.Encode());
|
||||
}
|
||||
|
||||
for (const auto& deleted_file_kvp : deleted_files_) {
|
||||
PutVarint32(dst, kDeletedFile);
|
||||
PutVarint32(dst, deleted_file_kvp.first); // level
|
||||
PutVarint64(dst, deleted_file_kvp.second); // file number
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < new_files_.size(); i++) {
|
||||
const FileMetaData& f = new_files_[i].second;
|
||||
PutVarint32(dst, kNewFile);
|
||||
PutVarint32(dst, new_files_[i].first); // level
|
||||
PutVarint64(dst, f.number);
|
||||
PutVarint64(dst, f.file_size);
|
||||
PutLengthPrefixedSlice(dst, f.smallest.Encode());
|
||||
PutLengthPrefixedSlice(dst, f.largest.Encode());
|
||||
}
|
||||
}
|
||||
|
||||
static bool GetInternalKey(Slice* input, InternalKey* dst) {
|
||||
Slice str;
|
||||
if (GetLengthPrefixedSlice(input, &str)) {
|
||||
return dst->DecodeFrom(str);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool GetLevel(Slice* input, int* level) {
|
||||
uint32_t v;
|
||||
if (GetVarint32(input, &v) && v < config::kNumLevels) {
|
||||
*level = v;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Status VersionEdit::DecodeFrom(const Slice& src) {
|
||||
Clear();
|
||||
Slice input = src;
|
||||
const char* msg = nullptr;
|
||||
uint32_t tag;
|
||||
|
||||
// Temporary storage for parsing
|
||||
int level;
|
||||
uint64_t number;
|
||||
FileMetaData f;
|
||||
Slice str;
|
||||
InternalKey key;
|
||||
|
||||
while (msg == nullptr && GetVarint32(&input, &tag)) {
|
||||
switch (tag) {
|
||||
case kComparator:
|
||||
if (GetLengthPrefixedSlice(&input, &str)) {
|
||||
comparator_ = str.ToString();
|
||||
has_comparator_ = true;
|
||||
} else {
|
||||
msg = "comparator name";
|
||||
}
|
||||
break;
|
||||
|
||||
case kLogNumber:
|
||||
if (GetVarint64(&input, &log_number_)) {
|
||||
has_log_number_ = true;
|
||||
} else {
|
||||
msg = "log number";
|
||||
}
|
||||
break;
|
||||
|
||||
case kPrevLogNumber:
|
||||
if (GetVarint64(&input, &prev_log_number_)) {
|
||||
has_prev_log_number_ = true;
|
||||
} else {
|
||||
msg = "previous log number";
|
||||
}
|
||||
break;
|
||||
|
||||
case kNextFileNumber:
|
||||
if (GetVarint64(&input, &next_file_number_)) {
|
||||
has_next_file_number_ = true;
|
||||
} else {
|
||||
msg = "next file number";
|
||||
}
|
||||
break;
|
||||
|
||||
case kLastSequence:
|
||||
if (GetVarint64(&input, &last_sequence_)) {
|
||||
has_last_sequence_ = true;
|
||||
} else {
|
||||
msg = "last sequence number";
|
||||
}
|
||||
break;
|
||||
|
||||
case kCompactPointer:
|
||||
if (GetLevel(&input, &level) && GetInternalKey(&input, &key)) {
|
||||
compact_pointers_.push_back(std::make_pair(level, key));
|
||||
} else {
|
||||
msg = "compaction pointer";
|
||||
}
|
||||
break;
|
||||
|
||||
case kDeletedFile:
|
||||
if (GetLevel(&input, &level) && GetVarint64(&input, &number)) {
|
||||
deleted_files_.insert(std::make_pair(level, number));
|
||||
} else {
|
||||
msg = "deleted file";
|
||||
}
|
||||
break;
|
||||
|
||||
case kNewFile:
|
||||
if (GetLevel(&input, &level) && GetVarint64(&input, &f.number) &&
|
||||
GetVarint64(&input, &f.file_size) &&
|
||||
GetInternalKey(&input, &f.smallest) &&
|
||||
GetInternalKey(&input, &f.largest)) {
|
||||
new_files_.push_back(std::make_pair(level, f));
|
||||
} else {
|
||||
msg = "new-file entry";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
msg = "unknown tag";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (msg == nullptr && !input.empty()) {
|
||||
msg = "invalid tag";
|
||||
}
|
||||
|
||||
Status result;
|
||||
if (msg != nullptr) {
|
||||
result = Status::Corruption("VersionEdit", msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string VersionEdit::DebugString() const {
|
||||
std::string r;
|
||||
r.append("VersionEdit {");
|
||||
if (has_comparator_) {
|
||||
r.append("\n Comparator: ");
|
||||
r.append(comparator_);
|
||||
}
|
||||
if (has_log_number_) {
|
||||
r.append("\n LogNumber: ");
|
||||
AppendNumberTo(&r, log_number_);
|
||||
}
|
||||
if (has_prev_log_number_) {
|
||||
r.append("\n PrevLogNumber: ");
|
||||
AppendNumberTo(&r, prev_log_number_);
|
||||
}
|
||||
if (has_next_file_number_) {
|
||||
r.append("\n NextFile: ");
|
||||
AppendNumberTo(&r, next_file_number_);
|
||||
}
|
||||
if (has_last_sequence_) {
|
||||
r.append("\n LastSeq: ");
|
||||
AppendNumberTo(&r, last_sequence_);
|
||||
}
|
||||
for (size_t i = 0; i < compact_pointers_.size(); i++) {
|
||||
r.append("\n CompactPointer: ");
|
||||
AppendNumberTo(&r, compact_pointers_[i].first);
|
||||
r.append(" ");
|
||||
r.append(compact_pointers_[i].second.DebugString());
|
||||
}
|
||||
for (const auto& deleted_files_kvp : deleted_files_) {
|
||||
r.append("\n RemoveFile: ");
|
||||
AppendNumberTo(&r, deleted_files_kvp.first);
|
||||
r.append(" ");
|
||||
AppendNumberTo(&r, deleted_files_kvp.second);
|
||||
}
|
||||
for (size_t i = 0; i < new_files_.size(); i++) {
|
||||
const FileMetaData& f = new_files_[i].second;
|
||||
r.append("\n AddFile: ");
|
||||
AppendNumberTo(&r, new_files_[i].first);
|
||||
r.append(" ");
|
||||
AppendNumberTo(&r, f.number);
|
||||
r.append(" ");
|
||||
AppendNumberTo(&r, f.file_size);
|
||||
r.append(" ");
|
||||
r.append(f.smallest.DebugString());
|
||||
r.append(" .. ");
|
||||
r.append(f.largest.DebugString());
|
||||
}
|
||||
r.append("\n}\n");
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace leveldb
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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_VERSION_EDIT_H_
|
||||
#define STORAGE_LEVELDB_DB_VERSION_EDIT_H_
|
||||
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/dbformat.h"
|
||||
|
||||
namespace leveldb {
|
||||
|
||||
class VersionSet;
|
||||
|
||||
struct FileMetaData {
|
||||
FileMetaData() : refs(0), allowed_seeks(1 << 30), file_size(0) {}
|
||||
|
||||
int refs;
|
||||
int allowed_seeks; // Seeks allowed until compaction
|
||||
uint64_t number;
|
||||
uint64_t file_size; // File size in bytes
|
||||
InternalKey smallest; // Smallest internal key served by table
|
||||
InternalKey largest; // Largest internal key served by table
|
||||
};
|
||||
|
||||
class VersionEdit {
|
||||
public:
|
||||
VersionEdit() { Clear(); }
|
||||
~VersionEdit() = default;
|
||||
|
||||
void Clear();
|
||||
|
||||
void SetComparatorName(const Slice& name) {
|
||||
has_comparator_ = true;
|
||||
comparator_ = name.ToString();
|
||||
}
|
||||
void SetLogNumber(uint64_t num) {
|
||||
has_log_number_ = true;
|
||||
log_number_ = num;
|
||||
}
|
||||
void SetPrevLogNumber(uint64_t num) {
|
||||
has_prev_log_number_ = true;
|
||||
prev_log_number_ = num;
|
||||
}
|
||||
void SetNextFile(uint64_t num) {
|
||||
has_next_file_number_ = true;
|
||||
next_file_number_ = num;
|
||||
}
|
||||
void SetLastSequence(SequenceNumber seq) {
|
||||
has_last_sequence_ = true;
|
||||
last_sequence_ = seq;
|
||||
}
|
||||
void SetCompactPointer(int level, const InternalKey& key) {
|
||||
compact_pointers_.push_back(std::make_pair(level, key));
|
||||
}
|
||||
|
||||
// Add the specified file at the specified number.
|
||||
// REQUIRES: This version has not been saved (see VersionSet::SaveTo)
|
||||
// REQUIRES: "smallest" and "largest" are smallest and largest keys in file
|
||||
void AddFile(int level, uint64_t file, uint64_t file_size,
|
||||
const InternalKey& smallest, const InternalKey& largest) {
|
||||
FileMetaData f;
|
||||
f.number = file;
|
||||
f.file_size = file_size;
|
||||
f.smallest = smallest;
|
||||
f.largest = largest;
|
||||
new_files_.push_back(std::make_pair(level, f));
|
||||
}
|
||||
|
||||
// Delete the specified "file" from the specified "level".
|
||||
void RemoveFile(int level, uint64_t file) {
|
||||
deleted_files_.insert(std::make_pair(level, file));
|
||||
}
|
||||
|
||||
void EncodeTo(std::string* dst) const;
|
||||
Status DecodeFrom(const Slice& src);
|
||||
|
||||
std::string DebugString() const;
|
||||
|
||||
private:
|
||||
friend class VersionSet;
|
||||
|
||||
typedef std::set<std::pair<int, uint64_t>> DeletedFileSet;
|
||||
|
||||
std::string comparator_;
|
||||
uint64_t log_number_;
|
||||
uint64_t prev_log_number_;
|
||||
uint64_t next_file_number_;
|
||||
SequenceNumber last_sequence_;
|
||||
bool has_comparator_;
|
||||
bool has_log_number_;
|
||||
bool has_prev_log_number_;
|
||||
bool has_next_file_number_;
|
||||
bool has_last_sequence_;
|
||||
|
||||
std::vector<std::pair<int, InternalKey>> compact_pointers_;
|
||||
DeletedFileSet deleted_files_;
|
||||
std::vector<std::pair<int, FileMetaData>> new_files_;
|
||||
};
|
||||
|
||||
} // namespace leveldb
|
||||
|
||||
#endif // STORAGE_LEVELDB_DB_VERSION_EDIT_H_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user