rsgt 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/Gemfile +6 -0
- data/README.markdown +139 -0
- data/Rakefile +10 -0
- data/bin/rsgt +5 -0
- data/lib/rsgt.rb +42 -0
- data/lib/rsgt/audio_extractor.rb +25 -0
- data/lib/rsgt/cli.rb +89 -0
- data/lib/rsgt/command_runner.rb +41 -0
- data/lib/rsgt/encrypted_steam_file.rb +44 -0
- data/lib/rsgt/multipacker.rb +114 -0
- data/lib/rsgt/multipacker/config_processor.rb +44 -0
- data/lib/rsgt/repacker.rb +37 -0
- data/lib/rsgt/rs_custom_song_toolkit.rb +41 -0
- data/lib/rsgt/saved_game.rb +37 -0
- data/lib/rsgt/saved_game/learn_a_song/song.rb +46 -0
- data/lib/rsgt/saved_game/score_attack/song.rb +38 -0
- data/lib/rsgt/saved_game/song_collection.rb +16 -0
- data/lib/rsgt/saved_game/statistics.rb +91 -0
- data/lib/rsgt/saved_game/statistics/song.rb +58 -0
- data/lib/rsgt/steam.rb +45 -0
- data/lib/rsgt/unpacked_psarc.rb +72 -0
- data/lib/rsgt/version.rb +3 -0
- data/lib/rsgt/vocals_extractor.rb +24 -0
- data/lib/rsgt/wav_shifter.rb +54 -0
- data/lib/rsgt/wwise_converter.rb +99 -0
- data/rsgt.gemspec +31 -0
- data/spec/lib/rsgt/audio_extractor_spec.rb +14 -0
- data/spec/lib/rsgt/command_runner_spec.rb +37 -0
- data/spec/lib/rsgt/repacker_spec.rb +24 -0
- data/spec/lib/rsgt/rs_custom_song_toolkit_spec.rb +47 -0
- data/spec/lib/rsgt/saved_game/learn_a_song/song_spec.rb +15 -0
- data/spec/lib/rsgt/saved_game_spec.rb +49 -0
- data/spec/lib/rsgt/unpacked_psarc_spec.rb +35 -0
- data/spec/lib/rsgt/vocals_extractor_spec.rb +14 -0
- data/spec/lib/rsgt/wav_shifter_spec.rb +30 -0
- data/spec/lib/rsgt/wwise_converter_spec.rb +22 -0
- data/spec/spec_helper.rb +11 -0
- metadata +168 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module RSGuitarTech
|
4
|
+
class Multipacker
|
5
|
+
class ConfigProcessor
|
6
|
+
|
7
|
+
attr_accessor :config
|
8
|
+
|
9
|
+
def initialize(opts)
|
10
|
+
@config = YAML.load_file opts[:config]
|
11
|
+
@packed = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def process!
|
15
|
+
config["repacks"].each do |config_data|
|
16
|
+
packer = packer_for config_data
|
17
|
+
packer.process!
|
18
|
+
packer.send(:psarcs).each do |packed_psarc|
|
19
|
+
@packed << packed_psarc.split("/").last.split(" _m").first
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def packer_for(config_data)
|
27
|
+
RSGuitarTech::Multipacker.new(
|
28
|
+
title: config_data["title"],
|
29
|
+
directory: config_data["directory"],
|
30
|
+
unpack_dir: config_data["unpack_dir"],
|
31
|
+
repack_dir: config_data["repack_dir"],
|
32
|
+
dest_dir: config["destination"],
|
33
|
+
options: {
|
34
|
+
reset_unpack: config_data["options"]["reset_unpack"],
|
35
|
+
reset_repack: config_data["options"]["reset_repack"]
|
36
|
+
},
|
37
|
+
filters: {
|
38
|
+
reject: @packed
|
39
|
+
}
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module RSGuitarTech
|
2
|
+
class Repacker
|
3
|
+
|
4
|
+
attr_accessor :psarc, :opts, :unpacked
|
5
|
+
|
6
|
+
def initialize(opts)
|
7
|
+
@psarc = File.expand_path(opts.delete :psarc)
|
8
|
+
@opts = opts
|
9
|
+
end
|
10
|
+
|
11
|
+
def repack!
|
12
|
+
UnpackedPSARC.from_psarc(psarc, opts) do |unpacked|
|
13
|
+
repack_vocals(unpacked) if opts[:vocals_xml]
|
14
|
+
repack_audio(unpacked) if opts[:audio]
|
15
|
+
unpacked.repack!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def repack_vocals(unpacked)
|
22
|
+
puts ".. Converting .xml to .sng"
|
23
|
+
CommandRunner.run! RSCustomSongToolkit.xml2sng(
|
24
|
+
manifest: unpacked.manifest(:vocals),
|
25
|
+
input: opts[:vocals_xml]
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def repack_audio(unpacked)
|
30
|
+
puts ".. Converting audio to .wem"
|
31
|
+
WWiseConverter.new(opts[:audio], opts).convert! do |converter|
|
32
|
+
FileUtils.cp converter.results[:main], unpacked.audio_track
|
33
|
+
FileUtils.cp converter.results[:preview], unpacked.audio_track(track: :preview) if opts[:preview]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module RSGuitarTech
|
2
|
+
class RSCustomSongToolkit
|
3
|
+
|
4
|
+
PCB_PATH = "/Applications/RocksmithCustomSongToolkit.app/Contents/Resources/packed_codebooks_aoTuV_603.bin"
|
5
|
+
|
6
|
+
def self.ww2ogg(file)
|
7
|
+
base_cmd "ww2ogg", [file, "--pcb", PCB_PATH]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.revorb(file)
|
11
|
+
base_cmd "revorb", [file]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.sng2xml(manifest:, input:)
|
15
|
+
sng2014 "--sng2xml", manifest: manifest, input: input
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.xml2sng(manifest:, input:)
|
19
|
+
sng2014 "--xml2sng", manifest: manifest, input: input
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.sng2014(cmd, manifest:, input:, arrangement: "Vocal", platform: "Mac")
|
25
|
+
base_cmd "sng2014", [
|
26
|
+
cmd,
|
27
|
+
"--manifest=#{manifest}",
|
28
|
+
"--input=#{input}",
|
29
|
+
"--arrangement=#{arrangement}",
|
30
|
+
"--platform=#{platform}"
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.base_cmd(tool, args)
|
35
|
+
[
|
36
|
+
"wine",
|
37
|
+
"/Applications/RocksmithCustomSongToolkit.app/Contents/Resources/#{tool}.exe"
|
38
|
+
] + args
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module RSGuitarTech
|
2
|
+
class SavedGame
|
3
|
+
|
4
|
+
attr_accessor :json, :filename
|
5
|
+
|
6
|
+
def self.from(filename)
|
7
|
+
file = File.open filename
|
8
|
+
profile = RSGuitarTech::EncryptedSteamFile.read(file)
|
9
|
+
self.new(profile.uncompressed_json, filename)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(json, filename)
|
13
|
+
@json = json
|
14
|
+
@filename = filename
|
15
|
+
end
|
16
|
+
|
17
|
+
def options
|
18
|
+
json["Options"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def statistics
|
22
|
+
@statistics ||= Statistics.new(json["Stats"])
|
23
|
+
end
|
24
|
+
|
25
|
+
def song_statistics
|
26
|
+
@song_statistics ||= SongCollection.new(Statistics, json["Stats"]["Songs"])
|
27
|
+
end
|
28
|
+
|
29
|
+
def score_attack
|
30
|
+
@score_attack ||= SongCollection.new(ScoreAttack, json["SongsSA"])
|
31
|
+
end
|
32
|
+
|
33
|
+
def learn_a_song
|
34
|
+
@learn_a_song ||= SongCollection.new(LearnASong, json["Songs"])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module RSGuitarTech
|
2
|
+
class SavedGame
|
3
|
+
module LearnASong
|
4
|
+
|
5
|
+
DynamicDifficulty = Struct.new(:avg, :phrases, :level_up) do
|
6
|
+
# Avg = one of [0.0, 1.0, 2.0, 3.0, 4.0]
|
7
|
+
# Phrases = a whole big thing
|
8
|
+
# LevelUp = array of tiny floats
|
9
|
+
end
|
10
|
+
|
11
|
+
PlaynextStats = Struct.new(:timestamp, :phrase_iterations, :chords, :articulations, :phrases) do
|
12
|
+
# TimeStamp = duh
|
13
|
+
# PhraseIterations
|
14
|
+
# Chords
|
15
|
+
# Articulations
|
16
|
+
# Phrases
|
17
|
+
end
|
18
|
+
|
19
|
+
class Song
|
20
|
+
attr_accessor :id, :timestamp, :dynamic_difficulty, :play_next_stats
|
21
|
+
|
22
|
+
def self.from(key, json)
|
23
|
+
self.new(
|
24
|
+
id: key,
|
25
|
+
timestamp: json["TimeStamp"],
|
26
|
+
dynamic_difficulty: json["DynamicDifficulty"],
|
27
|
+
play_next_stats: json["PlaynextStats"]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(id:, timestamp:, dynamic_difficulty:, play_next_stats:)
|
32
|
+
@id = id
|
33
|
+
@timestamp = timestamp
|
34
|
+
@dynamic_difficulty = dynamic_difficulty
|
35
|
+
@play_next_stats = play_next_stats
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
return false if other == nil
|
40
|
+
|
41
|
+
id == other.id
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module RSGuitarTech
|
2
|
+
class SavedGame
|
3
|
+
module ScoreAttack
|
4
|
+
|
5
|
+
PICKS = {
|
6
|
+
2.0 => "Bronze",
|
7
|
+
3.0 => "Silver",
|
8
|
+
4.0 => "Gold",
|
9
|
+
5.0 => "Platinum",
|
10
|
+
}
|
11
|
+
|
12
|
+
DIFFICULTIES = %w{Easy Medium Hard Master}
|
13
|
+
|
14
|
+
class Song
|
15
|
+
attr_accessor :id, :timestamp, :play_count, :picks, :high_scores
|
16
|
+
|
17
|
+
def self.from(key, json)
|
18
|
+
self.new(
|
19
|
+
id: key,
|
20
|
+
timestamp: json["TimeStamp"],
|
21
|
+
play_count: json["PlayCount"],
|
22
|
+
picks: json["Badges"],
|
23
|
+
high_scores: json["HighScores"],
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(id:, timestamp:, play_count:, picks:, high_scores:)
|
28
|
+
@id, @timestamp, @play_count, @picks, @high_scores = id, timestamp, play_count, picks, high_scores
|
29
|
+
end
|
30
|
+
|
31
|
+
def ==(other)
|
32
|
+
return false if other == nil
|
33
|
+
id == other.id
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module RSGuitarTech
|
2
|
+
class SavedGame
|
3
|
+
class SongCollection
|
4
|
+
attr_accessor :collection
|
5
|
+
|
6
|
+
delegate :values, :[], to: :collection
|
7
|
+
|
8
|
+
def initialize(klass, json)
|
9
|
+
@collection = json.inject({}) do |memo, (key, value)|
|
10
|
+
memo[key] = klass::Song.from(key, value)
|
11
|
+
memo
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module RSGuitarTech
|
2
|
+
class SavedGame
|
3
|
+
class Statistics
|
4
|
+
|
5
|
+
attr_accessor :json
|
6
|
+
|
7
|
+
def initialize(json)
|
8
|
+
@json = json
|
9
|
+
end
|
10
|
+
|
11
|
+
# Timing Section
|
12
|
+
def session_seconds
|
13
|
+
json['SessionTime']
|
14
|
+
end
|
15
|
+
|
16
|
+
def las_seconds
|
17
|
+
json['TimePlayed']
|
18
|
+
end
|
19
|
+
|
20
|
+
def games_seconds
|
21
|
+
json['GuitarcadePlayedTime'].map(&:values).flatten.inject(&:+)
|
22
|
+
end
|
23
|
+
|
24
|
+
def lesson_second
|
25
|
+
json['TimePlayedAndLesson'] - las_seconds
|
26
|
+
end
|
27
|
+
|
28
|
+
def rs_seconds
|
29
|
+
session_seconds + las_seconds + games_seconds + lesson_second
|
30
|
+
end
|
31
|
+
|
32
|
+
# / Timing Section
|
33
|
+
|
34
|
+
def missions_completed
|
35
|
+
json['TotalNumMissionCompletions'].to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
def session_count
|
39
|
+
json['SessionCnt'].to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
def songs_played_count
|
43
|
+
json['SongsPlayedCount'].to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
def songs_mastered_count
|
47
|
+
json['NumSongsMastered'].to_i
|
48
|
+
end
|
49
|
+
|
50
|
+
def dlc_played_count
|
51
|
+
json['DLCPlayedCount'].to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
def songs_lead_played_count
|
55
|
+
json['SongsLeadPlayedCount'].to_i
|
56
|
+
end
|
57
|
+
|
58
|
+
def songs_rhythm_played_count
|
59
|
+
json['SongsRhythmPlayedCount'].to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
def songs_bass_played_count
|
63
|
+
json['SongsBassPlayedCount'].to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
def session_mission_time
|
67
|
+
json['SessionMissionTime']
|
68
|
+
end
|
69
|
+
|
70
|
+
def longest_streak
|
71
|
+
json['Streak'].to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
def sa_songs_played_hard
|
75
|
+
json['SASongsPlayedHardAndMore'].to_i
|
76
|
+
end
|
77
|
+
|
78
|
+
def sa_songs_played_master
|
79
|
+
json['SASongsPlayedMaster'].to_i
|
80
|
+
end
|
81
|
+
|
82
|
+
def sa_songs_cleared_hard
|
83
|
+
json['SANumSongsClearedHard'].to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
def sa_songs_cleared_master
|
87
|
+
json['SANumSongsClearedMaster'].to_i
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module RSGuitarTech
|
2
|
+
class SavedGame
|
3
|
+
class Statistics
|
4
|
+
|
5
|
+
class Song
|
6
|
+
ATTRIBUTES = *%i{
|
7
|
+
id date_sa sa_platinum_point_awarded mastery_last sa_play_count played_count accuracy_global date_las accuracy_chords
|
8
|
+
mastery_previous mastery_peak mastery_previous_peak streak sa_win_count sa_general_point_awarded articulation_accuracy
|
9
|
+
chords_accuracies sa_fail_count score_attack_play_count
|
10
|
+
}
|
11
|
+
|
12
|
+
attr_accessor *ATTRIBUTES
|
13
|
+
|
14
|
+
def self.from(key, json)
|
15
|
+
sa_play_count = json["SAPlayCount"].map(&:values).flatten rescue []
|
16
|
+
self.new(
|
17
|
+
id: key,
|
18
|
+
date_sa: json["DateSA"],
|
19
|
+
sa_platinum_point_awarded: json["SAPlatinumPointAwarded"],
|
20
|
+
mastery_last: json["MasteryLast"],
|
21
|
+
sa_play_count: sa_play_count,
|
22
|
+
played_count: json["PlayedCount"],
|
23
|
+
accuracy_global: json["AccuracyGlobal"],
|
24
|
+
date_las: json["DateLAS"],
|
25
|
+
accuracy_chords: json["AccuracyChords"],
|
26
|
+
mastery_previous: json["MasteryPrevious"],
|
27
|
+
mastery_peak: json["MasteryPeak"],
|
28
|
+
mastery_previous_peak: json["MasteryPreviousPeak"],
|
29
|
+
streak: json["Streak"],
|
30
|
+
sa_win_count: json["SAWinCount"],
|
31
|
+
sa_general_point_awarded: json["SAGeneralPointAwarded"],
|
32
|
+
articulation_accuracy: json["ArticulationAccuracy"],
|
33
|
+
chords_accuracies: json["ChordsAccuracies"],
|
34
|
+
sa_fail_count: json["SAFailCount"],
|
35
|
+
score_attack_play_count: json["ScoreAttack_PlayCount"]
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.nil_song
|
40
|
+
self.new(
|
41
|
+
ATTRIBUTES.inject({}) { |memo, attr| memo[attr] = nil; memo }
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(args)
|
46
|
+
args.each do |k, v|
|
47
|
+
self.send("#{k}=", v)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def ==(other)
|
52
|
+
return false if other == nil
|
53
|
+
id == other.id
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/rsgt/steam.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module RSGuitarTech
|
2
|
+
class Steam
|
3
|
+
|
4
|
+
ROCKSMITH2014_APP_ID = 221680
|
5
|
+
|
6
|
+
def save_file
|
7
|
+
"#{base_path}#{save_filename}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def save_filename
|
11
|
+
"#{unique_id}_PRFLDB"
|
12
|
+
end
|
13
|
+
|
14
|
+
def local_profiles
|
15
|
+
"#{base_path}LocalProfiles.json"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Uplay junk
|
19
|
+
def crd
|
20
|
+
"#{base_path}crd"
|
21
|
+
end
|
22
|
+
|
23
|
+
def unique_id
|
24
|
+
@unique_id ||= begin
|
25
|
+
file = File.open local_profiles
|
26
|
+
data = RSGuitarTech::EncryptedSteamFile.read(file)
|
27
|
+
data.uncompressed_json['Profiles'].first['UniqueID']
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def base_path
|
34
|
+
"#{steam_path}#{steam_id}/#{ROCKSMITH2014_APP_ID}/remote/"
|
35
|
+
end
|
36
|
+
|
37
|
+
def steam_path
|
38
|
+
"/Users/#{`whoami`.strip}/Library/Application\ Support/Steam/userdata/"
|
39
|
+
end
|
40
|
+
|
41
|
+
def steam_id
|
42
|
+
Dir["#{steam_path}/*"].first.match(/\/(\d+)$/)[1]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|