Compare commits

...

2 Commits

Author SHA1 Message Date
mrbesen fd6f908671
reading basic structure 2021-06-01 21:06:20 +02:00
mrbesen 37edb342fa
basic hirachy, basic map loading 2021-06-01 11:47:41 +02:00
20 changed files with 433 additions and 44 deletions

View File

@ -7,7 +7,7 @@
NAME = libBeatsaber.a
NAMETEST = test
CFLAGS = -std=c++17 -O2 -pipe -Wall -Wextra -Wno-unused-parameter -Wpedantic -rdynamic
CFLAGS = -std=c++17 -O2 -pipe -Wall -Wextra -Wno-unused-parameter -Wpedantic -rdynamic -g
CXX = g++
SRCF = src/
BUILDDIR = build/
@ -20,25 +20,38 @@ INCLUDES = $(addprefix -I, $(INCFS))
LDFLAGS =
SRCFILES = $(shell find $(SRCF) -name "*.cpp")
OBJFILES = $(patsubst $(SRCF)%, $(BUILDDIR)%, $(patsubst %.cpp, %.o, $(SRCFILES))) $(LOGO)
OBJFILES = $(patsubst $(SRCF)%, $(BUILDDIR)%, $(patsubst %.cpp, %.o, $(SRCFILES)))
DEPFILES = $(wildcard $(DEPF)*.d)
SOURCEDIRS = $(shell find $(SRCF) -type d -printf "%p/\n")
BUILDDIRS = $(patsubst $(SRCF)%, $(BUILDDIR)%, $(SOURCEDIRS))
OBJFILESTEST = $(OBJFILES)
INCLUDES += $(addprefix -I, $(SOURCEDIRS))
# FEATURES
# zipFile support
DEFINES += -DBEATSABERZIPSUPPORT=1
# link zipios dynamic
LDFLAGS += -lzipios
# OR link zipios static (path to static lib)
#OBJFILES += /usr/local/lib/libzipios.a
#LDFLAGS += -lz #libzlib is still required dynamic
OBJFILESTEST = $(OBJFILES)
all: $(NAME) runtest
$(NAME): | $(BUILDDIRS) $(DEPF) $(OBJFILES)
$(NAME): $(OBJFILES) | $(BUILDDIRS) $(DEPF)
@$(AR) rvs $@ $(filter %.o, $^)
$(BUILDDIR)%.o: $(SRCF)%.cpp
$(BUILDDIR)%.o: $(SRCF)%.cpp | $(BUILDDIRS) $(DEPF)
@echo "Compiling: $@"
@$(CXX) $(CFLAGS) $(INCLUDES) $< -MM -MT $@ > $(DEPF)$(subst /,_,$*).d
@$(CXX) -c -o $@ $(CFLAGS) $(INCLUDES) $<
@$(CXX) $(CFLAGS) $(DEFINES) $(INCLUDES) $< -MM -MT $@ > $(DEPF)$(subst /,_,$*).d
@$(CXX) -c -o $@ $(CFLAGS) $(DEFINES) $(INCLUDES) $<
%/:
mkdir -p $@
@ -51,7 +64,7 @@ clean:
$(NAMETEST): $(BUILDDIRS) $(DEPF) $(TESTF)*.cpp $(OBJFILESTEST) $(NAME)
@echo "Compiling tests"
@$(CXX) -o $@ $(filter %.o, $^) $(filter %.cpp, $^) $(filter %.a, $^) $(CFLAGS) $(INCLUDES) $(LDFLAGS)
@$(CXX) -o $@ $(filter %.o, $^) $(filter %.cpp, $^) $(filter %.a, $^) $(CFLAGS) $(DEFINES) $(INCLUDES) $(LDFLAGS)
runtest: $(NAMETEST)
@echo "Running tests"

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# libBeatsaber
a small Library to read and write beatsaber level.

View File

@ -0,0 +1,43 @@
#pragma once
#include "beatlevel.h"
#include <nlohmann/json_fwd.hpp>
using json = nlohmann::json;
namespace Beatsaber {
class BeatLevelImpl : public BeatLevel, public std::enable_shared_from_this<BeatLevelImpl> {
protected:
std::weak_ptr<BeatSet> parent;
Difficulty::Difficulty dif;
int32_t diffRank;
std::string filename;
double njs;
double nso;
const json& base;
public:
BeatLevelImpl(std::weak_ptr<BeatSet> p, const json& j);
virtual ~BeatLevelImpl();
virtual Difficulty::Difficulty getDifficulty() const override;
virtual int32_t getDifficultyRank() const override;
virtual std::string getFilename() const override;
virtual double getNoteJumpSpeed() const override; // in m
virtual double getNoteStartOffset() const override;
virtual std::string getCustomData() const override;
//TODO: get level content
virtual void printDebug() const override;
virtual std::shared_ptr<BeatSet> getBeatSet() const override;
virtual std::shared_ptr<BeatMap> getBeatMap() const override;
};
}

View File

@ -2,6 +2,7 @@
#include "beatmap.h"
#include <memory>
#include <nlohmann/json.hpp>
#include "beatset.h"
@ -10,7 +11,7 @@ using json = nlohmann::json;
namespace Beatsaber {
class BeatMapImpl : public BeatMap {
class BeatMapImpl : public BeatMap, public std::enable_shared_from_this<BeatMapImpl> {
protected:
const std::string path;
@ -49,8 +50,8 @@ public:
virtual void printDebug() const override;
friend std::unique_ptr<BeatMap> BeatMap::loadFromFolder(const std::string& folderPath);
friend std::unique_ptr<BeatMap> BeatMap::loadFromZip(const std::string& zipPath);
friend std::shared_ptr<BeatMap> BeatMap::loadFromFolder(const std::string& folderPath);
friend std::shared_ptr<BeatMap> BeatMap::loadFromZip(const std::string& zipPath);
};
}

View File

@ -6,18 +6,24 @@ using json = nlohmann::json;
namespace Beatsaber {
class BeatSetImpl : public BeatSet {
class BeatSetImpl : public BeatSet, public std::enable_shared_from_this<BeatSetImpl> {
protected:
std::weak_ptr<BeatMap> parent;
BeatmapCharacteristic::BeatmapCharacteristic characteristic;
std::vector<std::shared_ptr<BeatLevel>> level;
public:
BeatSetImpl(const json& j);
BeatSetImpl(std::weak_ptr<BeatMap> p, const json& j);
BeatSetImpl(const BeatSetImpl& c) = default;
BeatSetImpl() = default;
virtual ~BeatSetImpl();
virtual BeatmapCharacteristic::BeatmapCharacteristic getCharacteristic() const;
virtual void printDebug() const override;
virtual BeatmapCharacteristic::BeatmapCharacteristic getCharacteristic() const override;
virtual std::shared_ptr<BeatMap> getBeatMap() const override;
};
}

View File

@ -0,0 +1,20 @@
#pragma once
#include <memory>
#include <string>
#include <istream>
// this classes form a abstraction to read from folders and zipfiles the same way. Maybe add other sources too?
namespace Beatsaber {
//Interface
class FileReader {
public:
virtual ~FileReader() {}
virtual std::shared_ptr<std::istream> getFileStream(const std::string& filename) = 0;
};
}

View File

@ -0,0 +1,38 @@
#pragma once
#include "filereader.h"
#if BEATSABERZIPSUPPORT == 1
#include <zipios/zipfile.hpp>
#endif
namespace Beatsaber {
//implementation for folders
class FolderReader : public FileReader {
protected:
std::string path;
public:
FolderReader(const std::string& path);
virtual ~FolderReader();
virtual std::shared_ptr<std::istream> getFileStream(const std::string& filename) override;
};
#if BEATSABERZIPSUPPORT == 1
//implementation for zipfiles
class ZipReader : public FileReader {
protected:
zipios::ZipFile file;
public:
ZipReader(const std::string& path);
virtual ~ZipReader();
virtual std::shared_ptr<std::istream> getFileStream(const std::string& filename) override;
};
#endif
}

View File

@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <memory>
#include "difficulties.h"
namespace Beatsaber {
//fwd
class BeatSet;
class BeatMap;
class BeatLevel {
public:
virtual ~BeatLevel() {}
virtual Difficulty::Difficulty getDifficulty() const = 0;
virtual int32_t getDifficultyRank() const = 0;
virtual std::string getFilename() const = 0;
virtual double getNoteJumpSpeed() const = 0; // in m
virtual double getNoteStartOffset() const = 0;
virtual std::string getCustomData() const = 0;
//TODO: get level content
virtual void printDebug() const = 0;
virtual std::shared_ptr<BeatSet> getBeatSet() const = 0; // parent
virtual std::shared_ptr<BeatMap> getBeatMap() const = 0; // grandparent
};
}

View File

@ -6,15 +6,12 @@
#include <vector>
#include "beatmapcharacteristic.h"
#include "beatlevel.h"
#include "beatset.h"
namespace Beatsaber {
class BeatLevel {
public:
};
class BeatMap {
public:
virtual ~BeatMap() {}
@ -43,8 +40,8 @@ public:
virtual void printDebug() const = 0;
static std::unique_ptr<BeatMap> loadFromFolder(const std::string& folderPath);
static std::unique_ptr<BeatMap> loadFromZip(const std::string& zipPath);
static std::shared_ptr<BeatMap> loadFromFolder(const std::string& folderPath);
static std::shared_ptr<BeatMap> loadFromZip(const std::string& zipPath);
};

View File

@ -20,5 +20,7 @@ namespace Beatsaber {
const std::uint8_t characteristicsNameSize = sizeof(characteristicsName)/sizeof(std::string);
BeatmapCharacteristic getByString(const std::string& str);
const std::string& toString(BeatmapCharacteristic c);
}
}

View File

@ -1,15 +1,23 @@
#pragma once
#include <memory>
#include "beatmapcharacteristic.h"
#include "beatlevel.h"
namespace Beatsaber {
//fwd decl.
class BeatMap;
class BeatSet {
public:
virtual ~BeatSet() {}
virtual void printDebug() const = 0;
virtual BeatmapCharacteristic::BeatmapCharacteristic getCharacteristic() const = 0;
virtual std::shared_ptr<BeatMap> getBeatMap() const = 0;
};
}

View File

@ -12,5 +12,13 @@ namespace Beatsaber {
EXPERT,
EXPERTP,
};
const std::string difficultyName[] {"None", "Easy", "Normal", "Hard", "Expert", "ExpertPlus"};
const std::uint8_t difficultyNameSize = sizeof(difficultyName)/sizeof(std::string);
Difficulty getByString(const std::string& str);
const std::string& toString(Difficulty d);
}
}

68
src/beatlevel.cpp Normal file
View File

@ -0,0 +1,68 @@
#include "beatlevelimpl.h"
#include <iostream>
#include <nlohmann/json.hpp>
#include "beatset.h"
namespace Beatsaber {
BeatLevelImpl::BeatLevelImpl(std::weak_ptr<BeatSet> p, const json& j) : parent(p), base(j) {
dif = Difficulty::getByString(j.value("_difficulty", ""));
diffRank = j.value("_difficultyRank", 0);
filename = j.value("_beatmapFilename", "");
njs = j.value("_noteJumpMovementSpeed", -1);
nso = j.value("_noteJumpStartBeatOffset", 0);
//load level content
if(!filename.empty()) {
}
}
BeatLevelImpl::~BeatLevelImpl() {}
Difficulty::Difficulty BeatLevelImpl::getDifficulty() const {
return dif;
}
int32_t BeatLevelImpl::getDifficultyRank() const {
return diffRank;
}
std::string BeatLevelImpl::getFilename() const {
return filename;
}
double BeatLevelImpl::getNoteJumpSpeed() const {
return njs;
}
double BeatLevelImpl::getNoteStartOffset() const {
return nso;
}
std::string BeatLevelImpl::getCustomData() const {
if(base.contains("_customData")) {
return base["_customData"].dump();
}
return "";
}
void BeatLevelImpl::printDebug() const {
std::cout <<
" Difficulty: " << Difficulty::toString(dif) << " (" << diffRank << ")"
<< "\n Filename: " << getFilename()
<< "\n njs: " << getNoteJumpSpeed() << " nso: " << getNoteStartOffset()
<< std::endl;
}
std::shared_ptr<BeatSet> BeatLevelImpl::getBeatSet() const {
return parent.lock();
}
std::shared_ptr<BeatMap> BeatLevelImpl::getBeatMap() const {
return parent.lock()->getBeatMap();
}
}

View File

@ -16,7 +16,6 @@ bool BeatMapImpl::load() {
try {
info >> infobase;
//load the beatset
const json& beatsets = infobase["_difficultyBeatmapSets"];
if(beatsets.is_array()) {
@ -24,12 +23,14 @@ bool BeatMapImpl::load() {
//load all the beatsets
for(const json& jsonbeatset : beatsets) {
beatSets.push_back(std::make_shared<BeatSetImpl>(jsonbeatset));
beatSets.push_back(std::make_shared<BeatSetImpl>(weak_from_this(), jsonbeatset));
}
}
return true;
} catch(...) {} //catch any json errors
} catch(const nlohmann::detail::exception& e) { //catch any json errors
std::cout << "Could not Read BeatMap: " << e.what() << std::endl;
}
return false;
}
@ -98,7 +99,10 @@ std::string BeatMapImpl::get360EnvName() const {
}
std::string BeatMapImpl::getCustomData() const {
return infobase["_customData"].dump();
if(infobase.contains("_customData")) {
return infobase["_customData"].dump();
}
return "";
}
const std::vector<std::shared_ptr<BeatSet>>& BeatMapImpl::getBeatSets() const {
@ -129,30 +133,40 @@ std::shared_ptr<BeatSet> BeatMapImpl::getBeatSet(BeatmapCharacteristic::BeatmapC
void BeatMapImpl::printDebug() const {
std::cout << "SongName: " << getSongName() << " - " << getSongAuthorName() << " (" << getSubName() << ")"
<< "\nMapper: " << getMapperName()
<< "\nVersion: " << getVersion()
<< "\nBPM: " << getBPM() << " offset: " << getTimeOffset()
<< "\nSchuffle " << getSchuffle() << " period: " << getSchufflePeriod()
<< "\nPreviewStart: " << getPreviewStart() << " duration: " << getPreviewDuration()
<< "\nSongFile: " << getSongFilename()
<< "\nImageFile: " << getImageFilename()
<< "\nEnvName: " << getEnvName() << " 360Env: " << get360EnvName()
<< "\ncustomData: " << getCustomData()
<< "\nBeatSetCount: " << getBeatSetCount()
<< "\n Mapper: " << getMapperName()
<< "\n Version: " << getVersion()
<< "\n BPM: " << getBPM() << " offset: " << getTimeOffset()
<< "\n Schuffle " << getSchuffle() << " period: " << getSchufflePeriod()
<< "\n PreviewStart: " << getPreviewStart() << " duration: " << getPreviewDuration()
<< "\n SongFile: " << getSongFilename()
<< "\n ImageFile: " << getImageFilename()
<< "\n EnvName: " << getEnvName() << " 360Env: " << get360EnvName()
<< "\n customData: " << getCustomData()
<< "\n BeatSetCount: " << getBeatSetCount()
<< std::endl;
for(auto it : beatSets) {
it->printDebug();
}
}
std::unique_ptr<BeatMap> BeatMap::loadFromFolder(const std::string& folderPath) {
auto map = std::make_unique<BeatMapImpl>(folderPath, false);
std::shared_ptr<BeatMap> BeatMap::loadFromFolder(const std::string& folderPath) {
auto map = std::make_shared<BeatMapImpl>(folderPath, false);
if(!map->load()) return nullptr;
return map;
}
std::unique_ptr<BeatMap> BeatMap::loadFromZip(const std::string& zipPath) {
auto map = std::make_unique<BeatMapImpl>(zipPath, true);
std::shared_ptr<BeatMap> BeatMap::loadFromZip(const std::string& zipPath) {
#if BEATSABERZIPSUPPORT == 1
auto map = std::make_shared<BeatMapImpl>(zipPath, true);
if(!map->load()) return nullptr;
return map;
#else
// Zip not supported
std::cerr << "Zip not supported" << std::endl;
return nullptr;
#endif
}
}

View File

@ -1,16 +1,47 @@
#include "beatsetimpl.h"
#include <iostream> //debug print
#include <nlohmann/json.hpp>
#include "beatlevelimpl.h"
namespace Beatsaber {
BeatSetImpl::BeatSetImpl(const json& j) : characteristic(BeatmapCharacteristic::getByString(j["_beatmapCharacteristicName"])) {
BeatSetImpl::BeatSetImpl(std::weak_ptr<BeatMap> p, const json& j) : parent(p) {
try {
characteristic = BeatmapCharacteristic::getByString(j["_beatmapCharacteristicName"]);
//load level
const json& arr = j["_difficultyBeatmaps"];
if(arr.is_array()) {
level.reserve(arr.size());
std::cout << "Try to load: " << arr.size() << std::endl;
for(const json& beat : arr) {
level.push_back(std::make_shared<BeatLevelImpl>(weak_from_this(), beat));
}
}
} catch(std::exception& e) {
std::cout << "Could not read BeatSet: " << e.what() << std::endl;
characteristic = BeatmapCharacteristic::NONE;
}
}
BeatSetImpl::~BeatSetImpl() {}
void BeatSetImpl::printDebug() const {
std::cout << " Characteristic: " << BeatmapCharacteristic::toString(characteristic) << std::endl;
for(auto it : level) {
it->printDebug();
}
}
BeatmapCharacteristic::BeatmapCharacteristic BeatSetImpl::getCharacteristic() const {
return characteristic;
}
std::shared_ptr<BeatMap> BeatSetImpl::getBeatMap() const {
return parent.lock();
}
}

View File

@ -1,4 +1,5 @@
#include "beatmapcharacteristic.h"
#include "difficulties.h"
namespace Beatsaber {
namespace BeatmapCharacteristic {
@ -12,5 +13,29 @@ BeatmapCharacteristic getByString(const std::string& str) {
return BeatmapCharacteristic::NONE;
}
const std::string& toString(BeatmapCharacteristic c) {
uint8_t ic = (uint8_t) c;
if(ic >= characteristicsNameSize) return characteristicsName[0]; //none
return characteristicsName[ic];
}
}
namespace Difficulty {
Difficulty getByString(const std::string& str) {
for(uint8_t i = 0; i < difficultyNameSize; ++i) {
if(str == difficultyName[i]) { //TODO equals ignore Case
return (Difficulty) i;
}
}
return Difficulty::NONE;
}
const std::string& toString(Difficulty d) {
uint8_t id = (uint8_t) d;
if(id >= difficultyNameSize) return difficultyName[0]; //none
return difficultyName[id];
}
}
}

34
src/filereader.cpp Normal file
View File

@ -0,0 +1,34 @@
#include "filereaderimpl.h"
#include <fstream>
namespace Beatsaber {
FolderReader::FolderReader(const std::string& path) : path(path) {
// make path end with /
if(path.rfind('/') != path.size()-1)
this->path += "/";
}
FolderReader::~FolderReader() {}
std::shared_ptr<std::istream> FolderReader::getFileStream(const std::string& filename) {
std::shared_ptr<std::istream> stream = std::make_shared<std::ifstream>(path + filename);
return stream;
}
#if BEATSABERZIPSUPPORT == 1
ZipReader::ZipReader(const std::string& path) : file(path) {}
ZipReader::~ZipReader() {
file.close();
}
std::shared_ptr<std::istream> ZipReader::getFileStream(const std::string& filename) {
return file.getInputStream(filename);
}
#endif
}

View File

@ -4,8 +4,12 @@
#include "beatmap.h"
//tests
int difficulties_test();
int characteristics_test();
test_t tests[] = {NULL};
test_t tests[] = {
difficulties_test, characteristics_test,
NULL};
int main(int argc, char** argv) {
@ -26,7 +30,7 @@ int main(int argc, char** argv) {
printf("\033[1;93m%d\033[0;1m/%d failed\n", failcount, testcount);
//simple read test
std::unique_ptr<Beatsaber::BeatMap> bmap = Beatsaber::BeatMap::loadFromFolder("/media/satassd/steam/steamapps/common/Beat Saber/Beat Saber_Data/CustomLevels/1dd (Portal - Still Alive (Uppermost Remix) - kryptikos)");
std::shared_ptr<Beatsaber::BeatMap> bmap = Beatsaber::BeatMap::loadFromFolder("/media/satassd/steam/steamapps/common/Beat Saber/Beat Saber_Data/CustomLevels/1dd (Portal - Still Alive (Uppermost Remix) - kryptikos)");
if(!bmap) std::cout << "Could not load File" << std::endl;
else bmap->printDebug();

View File

@ -6,6 +6,7 @@
#define TESTDATA "./tests/data/"
#define ASSERT(BED, ERR) if(!(BED)) { std::cout << __FILE__ << ":" << __LINE__ << " " << ERR << std::endl; return TESTFAILED; }
// #define ASSERT(BED) ASSERT(BED, "")
#define CMPASSERTE(A, B, ERR) if( !((A) == (B))) { std::cout << __FILE__ << ":" << __LINE__ << " is: \"" << (A) << "\" should: \"" << (B) << "\""<< std::endl; return TESTFAILED; }
#define CMPASSERT(A, B) CMPASSERTE(A, B, "")
typedef int (*test_t)();

38
tests/testenums.cpp Normal file
View File

@ -0,0 +1,38 @@
#include "test.h"
#include "difficulties.h"
#include "beatmapcharacteristic.h"
int difficulties_test() {
using d = Beatsaber::Difficulty::Difficulty;
using namespace Beatsaber::Difficulty;
CMPASSERT(d::EASY, getByString("Easy"));
CMPASSERT(d::NORMAL, getByString("Normal"));
CMPASSERT(d::NONE, getByString(""));
CMPASSERT(d::NONE, getByString("abc"));
CMPASSERT(d::HARD, getByString("Hard"));
CMPASSERT(d::EXPERTP, getByString("ExpertPlus"));
return TESTGOOD;
}
int characteristics_test() {
using c = Beatsaber::BeatmapCharacteristic::BeatmapCharacteristic;
using namespace Beatsaber::BeatmapCharacteristic;
CMPASSERT(c::NONE, getByString(""));
CMPASSERT(c::NONE, getByString("abc"));
CMPASSERT(c::STANDARD, getByString("Standard"));
CMPASSERT(c::ONESABER, getByString("OneSaber"));
CMPASSERT(c::NOARROWS, getByString("NoArrows"));
CMPASSERT(c::DEGREE90, getByString("90Degree"));
CMPASSERT(c::DEGREE360, getByString("360Degree"));
return TESTGOOD;
}