wshuo 发表于 2021-11-9 20:16

写一个杀戮尖塔存档修改器

本帖最后由 wshuo 于 2021-11-9 20:23 编辑

[软件及源码下载](https://wshuo.lanzoui.com/izqRXwbziri)

### 1. 前言

之前杀戮尖塔打折了,然后 买了这个游戏,游戏很好玩,所以我简单研究了一下游戏。游戏是java写的,那么几乎可以看到他的完整源码了。   这个软件前前后后我差不多我写了一周了。所以写(水)一篇文章记录一下。

### 2.存档加密算法分析

首先我们保存一个 **铁甲战士** 的存档,存档路径在 `saves\IRONCLAD.autosave` 。打开文件大致可以看到是经过**base64**编码过的。用 **java Decompiler** 打开主程序 **desktop-1.0.jar**, 搜索 **IRONCLAD.autosave** 关键字:

!(https://img-blog.csdnimg.cn/20211102165111566.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Nob3V6aG91OTcwMQ==,size_16,color_FFFFFF,t_70)

找到了 **saveHelper.class** 这个类,在这个类的引用中函数中附件查看,最终我定位到了**SaveFileObfuscator**这个类(或许直接搜索base64比较快):

!(https://img-blog.csdnimg.cn/20211102165727560.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Nob3V6aG91OTcwMQ==,size_16,color_FFFFFF,t_70)

可以看到,这个加密算法就是将原始数据,与字符串**key**进行循环亦或, 然后在进行**base64** 编码,那么解密就是反过来,先**base64**解码,然后再亦或。   

补充一点亦或知识:

A ^ B = C

可得:

A ^ C = B

B ^ C = A

python 仿写:

```python
from base64 import b64decode,b64encode
import json

def decode(data):
    result = ""
    key = "key"
    data = b64decode(data)
    for index,i in enumerate(data):   
      result += chr(i ^ ord(key))
    jsonR = json.loads(result)
    return jsonR

def encode(data):
    data = json.dumps(data)
    result = ""
    key = "key"
    for index,i in enumerate(data.encode()):   
      result += chr(i ^ ord(key))

    result = b64encode(result.encode()).decode()
    return result

file = "IRONCLAD.autosave"
with open(file,"rb") as f:
    data = f.read()

data = decode(data)
print(data)
```

输出:

```json
{'shuffle_seed_count': 0, 'metric_purchased_purges': 0, 'metric_path_per_floor': ['M'], 'monster_list': ['Small Slimes', 'Jaw Worm', 'Looter', 'Gremlin Gang', 'Red Slaver', '2 Fungi Beasts', 'Looter', 'Exordium Thugs', '3 Louse', 'Large Slime', 'Exordium Wildlife', '2 Fungi Beasts', 'Lots of Slimes', '3 Louse', 'Large Slime'], 'metric_potions_floor_spawned': [], 'daily_mods': [], 'metric_campfire_choices': [], 'is_ascension_mode': False, 'metric_items_purchased': [], 'is_endless_mode': False, 'merchant_seed_count': 0, 'floor_num': 2, 'uncommon_relics': ['Meat on the Bone', 'Bottled Flame', 'Shuriken', 'Pantograph', 'Darkstone Periapt', 'Pear', 'Question Card', 'Letter Opener', 'Bottled Lightning', 'Frozen Egg 2', 'White Beast Statue', 'Mummified Hand', 'Paper Frog', 'Ornamental Fan', 'Sundial', 'HornCleat', 'Molten Egg 2', 'Gremlin Horn', 'Mercury Hourglass', 'Self Forming Clay', 'Matryoshka', 'InkBottle', 'Bottled Tornado', 'Eternal Feather', 'Toxic Egg 2', 'Kunai'], 'post_combat': False, 'combo': False, 'relics': ['Burning Blood', 'NeowsBlessing'], 'rare_relics': ['Thread and Needle', 'Incense Burner', 'StoneCalendar', 'Mango', 'Prayer Wheel', 'Ginger', "Charon's Ashes", 'TungstenRod', 'Old Coin', 'FossilizedHelix', 'CaptainsWheel', 'Champion Belt', 'Magic Flower', 'Torii', 'Girya', 'Pocketwatch', 'Gambling Chip', 'Unceasing Top', 'Ice Cream', 'Peace Pipe', 'Lizard Tail', 'Shovel', 'WingedGreaves', 'Bird Faced Urn', 'Calipers'], 'level_name': 'Exordium', 'metric_campfire_rested': 0, 'max_orbs': 0, 'boss': 'The Guardian', 'seed': 6.908306042847218e+18, 'metric_current_hp_per_floor': , 'current_health': 80, 'common_relics': ['Bag of Marbles', 'Bag of Preparation', 'Centennial Puzzle', 'MawBank', 'Lantern', 'Ancient Tea Set', 'Boot', 'Strawberry', 'Orichalcum', 'MealTicket', 'Bronze Scales', 'Dream Catcher', 'Blood Vial', 'Whetstone', 'Vajra', 'Nunchaku', 'Happy Flower', 'PreservedInsect', 'Toy Ornithopter', 'Regal Pillow', 'Anchor', 'Omamori', 'War Paint', 'Pen Nib', 'Potion Belt', 'Red Skull', 'Juzu Bracelet', 'Oddly Smooth Stone'], 'monsters_killed': 1, 'gold': 118, 'neow_bonus': 'THREE_ENEMY_KILL', 'card_random_seed_count': 0, 'card_seed_count': 9, 'is_daily': False, 'metric_campfire_rituals': 0, 'metric_card_choices': [{'not_picked': ['Thunderclap', 'Cleave', 'Twin Strike'], 'picked': 'SKIP', 'floor': 1}], 'metric_potions_obtained': [], 'is_final_act_on': False, 'treasure_seed_count': 1, 'metric_event_choices': [], 'current_room': 'com.megacrit.cardcrawl.rooms.ShopRoom', 'has_emerald_key': False, 'boss_relics': ['Cursed Key', 'Calling Bell', 'Ectoplasm', 'Sozu', 'SlaversCollar', 'Mark of Pain', 'Snecko Eye', "Philosopher's Stone", 'Black Blood', 'Runic Cube', 'Busted Crown', 'Runic Dome', 'Runic Pyramid', 'Tiny House', 'Black Star', 'Velvet Choker', 'Astrolabe', 'Coffee Dripper', 'Fusion Hammer', 'SacredBark', 'Empty Cage'], 'blue': 0, 'path_y': , 'path_x': , 'metric_item_purchase_floors': [], 'gold_gained': 19, 'ascension_level': 0, 'one_time_event_list': ['Accursed Blacksmith', 'Bonfire Elementals', 'Designer', 'Duplicator', 'FaceTrader', 'Fountain of Cleansing', 'Knowing Skull', 'Lab', "N'loth", 'NoteForYourself', 'SecretPortal', 'The Joust', 'WeMeetAgain', 'The Woman in Blue'], 'card_random_seed_randomizer': 2, 'metric_campfire_meditates': 0, 'perfect': 0, 'mugged': False, 'metric_build_version': '2020-12-22', 'event_list': ['Big Fish', 'The Cleric', 'Dead Adventurer', 'Golden Idol', 'Golden Wing', 'World of Goop', 'Liars Game', 'Living Wall', 'Mushrooms', 'Scrap Ooze', 'Shining Light'], 'elite_monster_list': ['3 Sentries', 'Gremlin Nob', 'Lagavulin', '3 Sentries', 'Lagavulin', '3 Sentries', 'Gremlin Nob', 'Lagavulin', '3 Sentries', 'Lagavulin'], 'metric_seed_played': '6908306042847217830', 'red': 3, 'elites3_killed': 0, 'relic_counters': [-1, 2], 'seed_set': False, 'elites2_killed': 0, 'metric_items_purged_floors': [], 'potion_seed_count': 1, 'blights': [], 'elites1_killed': 0, 'overkill': False, 'neow_cost': 'NONE', 'metric_floor_reached': 2, 'champions': 0, 'smoked': False, 'spirit_count': 0, 'special_seed': 0, 'potion_chance': 10, 'shop_relics': ['PrismaticShard', "Lee's Waffle", 'Chemical X', 'Frozen Eye', 'DollysMirror', 'Orrery', 'Sling', 'Strange Spoon', 'HandDrill', 'TheAbacus', 'Toolbox', 'Brimstone', 'Cauldron', 'OrangePellets', 'Membership Card', 'ClockworkSouvenir', 'Medical Kit'], 'custom_mods': [], 'metric_path_taken': ['M', '$'], 'name': 'wshuo', 'has_ruby_key': False, 'metric_playtime': 82, 'has_sapphire_key': False, 'is_trial': False, 'metric_max_hp_per_floor': , 'save_date': 1634731350300, 'cards': [{'upgrades': 0, 'id': 'Perfected Strike'}, {'upgrades': 0, 'id': 'Perfected Strike'}, {'upgrades': 0, 'id': 'Perfected Strike'}, {'upgrades': 0, 'id': 'Perfected Strike'}, {'upgrades': 0, 'id': 'Perfected Strike'}], 'endless_increments': [], 'metric_items_purged': [], 'purgeCost': 75, 'room_x': 0, 'blight_counters': [], 'room_y': 1, 'metric_gold_per_floor': , 'metric_potions_floor_usage': [], 'boss_list': ['The Guardian', 'Hexaghost', 'Slime Boss'], 'ai_seed_count': 0, 'event_chances': , 'metric_boss_relics': [], 'relic_seed_count': 5, 'act_num': 1, 'metric_damage_taken': [{'damage': 0, 'enemies': '2 Louse', 'floor': 1, 'turns': 1}], 'green': 0, 'mystery_machine': 0, 'metric_campfire_upgraded': 0, 'potion_slots': 3, 'chose_neow_reward': False, 'event_seed_count': 0, 'metric_relics_obtained': [], 'daily_date': 0, 'obtained_cards': {}, 'play_time': 82, 'hand_size': 5, 'max_health': 80, 'monster_seed_count': 37, 'potions': ['Potion Slot', 'Potion Slot', 'Potion Slot']}
```

说几个有用的参数:

| 参数               | 说明                                                         |
| ------------------ | ------------------------------------------------------------ |
| current_health   | 目前血量                                                   |
| max_health         | 最大血量                                                   |
| gold               | 金币                                                         |
| relics             | 遗物                                                         |
| cards            | 当前卡牌,upgrades表示是否升级                               |
| metric_seed_played | 本局游戏种子(这个在存档中是long long型,游戏中显示是字符串,后续我会有分析) |
| hand_size          | 手牌数量(最大不能大于10如果大于10,还是会按照10来算)       |
| red                | 能量点                                                       |

简单修改一下red:   

```python
data['red'] = 300
data = encode(data)
print(data)
```

将结果拷贝回**IRONCLAD.autosave** 文件,进入游戏:

!(https://img-blog.csdnimg.cn/20211102172548696.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Nob3V6aG91OTcwMQ==,size_16,color_FFFFFF,t_70)



可以看到已经修改成功了。

到此这个加密解密存档的算法算是写好了。但是我的研究没有到此结束,我想写一个可以通过拖拽全部卡牌来实现给存档中添加自定义卡牌的软件。所以后续我分析了卡牌和遗物的图片资源。

### 3.卡牌资源获取

将 **desktop-1.0.jar** 改名zip 然后解压可以得到卡牌图片资源,卡牌大小资源有两种分辨率的图片,这里我用的是小图片(小图片足够了),在 `desktop-1.0\cards`存有卡牌的资源。图片的信息存储在 **cards.atlas**文件中,这是一个文本文件,可以直接打开。

```
cards.png
size: 2048,2048
format: RGBA8888
filter: Linear,Linear
repeat: none
blue/skill/aggregate
rotate: false
xy: 491, 1831
size: 244, 182
orig: 250, 190
offset: 2, 8
index: -1
blue/skill/amplify
rotate: false
xy: 1226, 1831
size: 244, 182
orig: 250, 190
offset: 2, 8
index: -1
```

可以看到这个文件格式是,不同的png之间用一个空行分割,然后就是具体的卡牌名字,因为一个png图片中包含多个卡牌的画面,所以这个文件主要存放每张卡牌图片在png中的位置,其中利用xy 和size就可以实现将多个将png分割多个卡牌图片文件,但是这种cards.atlas,是不利于我们进行调用的,所以我写了一个脚本将其处理成 json:

`parseAtlas.py`

```python
def parseAtlas(filePath):
    result = []
    with open(filePath) as f:
      data = f.readline()
      while True:
            if data == "":
                break
            if data == "\n":
                tempContent = []
                pngContent = dict(
                name = f.readline().strip(),
                size = f.readline().strip().split(":")[-1].strip(),
                format = f.readline().strip().split(":")[-1].strip(),
                filter = f.readline().strip().split(":")[-1].strip(),
                repeat = f.readline().strip().split(":")[-1].strip(),
                content = tempContent,
                  )
                while True:
                  data = f.readline()
                  if data == "":
                        break
                  if data == "\n":
                        break
                  temp = dict(
                        name = data.strip(),
                        rotate = f.readline().strip().split(":")[-1].strip(),
                        xy = .strip().split(",")],
                        size = .strip().split(",")],
                        orig = f.readline().strip().split(":")[-1].strip(),
                        offset = f.readline().strip().split(":")[-1].strip(),
                        index = f.readline().strip().split(":")[-1].strip(),
                  )   
                  tempContent.append(temp)
                result.append(pngContent)
    return result


if __name__ == '__main__':
    filePath = "cards.atlas"
    print(parseAtlas(filePath))
```

然后利用这些信息对png图片进行分割并保存,这里我图片名字用的是将**/**替换为**_** 这样可以尽可能保存原始信息,例如 **blue/skill/amplify**变为 **blue_skill_amplify.png**   

`saveCard.py`

```python
from parseAtlas import parseAtlas
import cv2
import os

filePath = "cards.atlas"
s = parseAtlas(filePath)
# print(s)
for role in s:
    print(role["name"])
    for card in role['content']:
      img = cv2.imread(role["name"],cv2.IMREAD_UNCHANGED)
      path = os.path.join("card",card["name"].replace("/","_") +".png")
      cv2.imwrite(path,img:card["size"]+card["xy"],card["xy"]:card["size"]+card["xy"]])
      # print(card)
    # break
```

保存完图片大概是这样:

!(https://img-blog.csdnimg.cn/20211103013103151.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Nob3V6aG91OTcwMQ==,size_16,color_FFFFFF,t_70)

当然这距离最终卡牌还有一些距离。

对于卡牌我们还需要卡牌的外框,以及很多细节,这些资源都在`desktop-1.0\cardui`中,当然还是有cards.atlas文件,所以这里可以复用保存卡牌的那部分代码,不过这里我只保存了我需要的,因为有两种分辨率的,所以我只保存了低分辨率的 cardui。

!(https://img-blog.csdnimg.cn/20211103013513631.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Nob3V6aG91OTcwMQ==,size_16,color_FFFFFF,t_70)

有了这些我们还差文字,也就是卡牌说明信息,卡牌信息在 `desktop-1.0\localization\zhs\cards.json`中:

```json
{
"A Thousand Cuts": {
    "NAME": "凌迟",
    "DESCRIPTION": "你每打出一张牌,就对所有敌人造成 !M! 点伤害。"
},
"Accuracy": {
    "NAME": "精准",
    "DESCRIPTION": "*小刀 造成的伤害增加 !M! 。"
},
```

这里**A Thousand Cuts**就是cardId,相当于每张卡牌的唯一标识 ,也是存档需要的东西(存档最后添加的就是这种卡牌名字),但是这里有一些数字是没有写的例如其中的 !M!, 这些只能上原始的代码,我们不能直接解析 .class文件,但是可以解析 .java 文件, 所以这一步我用到了 jad 工具,用命令行的方式将 .class 反编译回 .jad(其实就是.java文件)文件,这样我就可以解析出我需要的信息了,寻找对应卡牌类的方法我不细说了,位置在 `\desktop-1.0\com\megacrit\cardcrawl\cards`,这其中还有很多测试未开放的卡牌类,当然我只需要我想要的几类卡牌,红色,绿色,蓝色,紫色,无色,诅咒,不同颜色对应人物(无色所有人物都可以使用,当然诅咒也是),将其对应文件夹拷贝的我的项目目录,然后写一个脚本进行批量反编译。

`convertJad.py`

```python
import os
import sys
rootPath = "cards_source"

for secondPath in os.listdir(rootPath):
    os.chdir(os.path.join(rootPath,secondPath))
    for file in os.listdir("."):
      os.system(f"Jad {file}")
    os.chdir("../..")
```

打开一个反编译的文件简单分析一下:

```java
public class Aggregate extends AbstractCard
{

    public Aggregate()
    {
      super("Aggregate", cardStrings.NAME, "blue/skill/aggregate", 1, cardStrings.DESCRIPTION, com.megacrit.cardcrawl.cards.AbstractCard.CardType.SKILL, com.megacrit.cardcrawl.cards.AbstractCard.CardColor.BLUE, com.megacrit.cardcrawl.cards.AbstractCard.CardRarity.UNCOMMON, com.megacrit.cardcrawl.cards.AbstractCard.CardTarget.SELF);
      baseMagicNumber = 4;
      magicNumber = baseMagicNumber;
    }

    public void use(AbstractPlayer p, AbstractMonster m)
    {
      addToBot(new AggregateEnergyAction(magicNumber));
    }

    public void upgrade()
    {
      if(!upgraded)
      {
            upgradeName();
            upgradeMagicNumber(-1);
            initializeDescription();
      }
    }

    public AbstractCard makeCopy()
    {
      return new Aggregate();
    }

    public static final String ID = "Aggregate"; //cardID信息
    private static final CardStrings cardStrings;

    static
    {
      cardStrings = CardCrawlGame.languagePack.getCardStrings("Aggregate");
    }
}

```

可以看到 这里也是包含 cardID信息的,我们需要的信息在super位置,这里不能将 super的第1个参数作为cardID, 因为那个是类名,类的名字是不能具有空格的,但是cardID 是有可能具有空格的,super第2个参数是图片资源位置,这里可以对应上我们保存的图片,第4个参数就是卡牌需要的能量点,第6个参数是卡牌的类型(能力,技能,攻击,诅咒,状态,等等),第7个参数是卡牌的颜色(也相当于所属人物),第8个参数是卡牌的稀有度(普通,不普通,稀有,等等)。后面的`baseMagicNumber = 4`和 `magicNumber = baseMagicNumber` 也是我们需要的信息,这些信息替换卡牌文字说明的一些符号,类似 !M! 这类的。那么目前可以将这些信息联系起来了,可以通过 cardID信息将卡牌的文字说明与 jad文件代码中的信息联系起来,jad中具有卡牌的一些详细信息和图片资源位置,通过正则匹配jad文件中的信息将需要的信息拿出来了并结合文字说明的信息合并到一起:

```getInfoFromJad.py```

```python
import os
import sys
import re
import json


def info():
    rootPath = "cards_source"
    with open("cards.json",encoding="utf8") as f:
      data = f.read()

    cardJosn = json.loads(data)

    result = []
    for secondPath in os.listdir(rootPath):
      os.chdir(os.path.join(rootPath,secondPath))
      for file in os.listdir("."):

            if file.endswith(".jad"):
                with open(file,encoding="gbk") as f:
                  SourceStr = f.read()
                cardId = re.findall('public static final String ID = "(.*?)";',SourceStr)
                s = re.findall('super\(".*?", cardStrings.NAME, "(.*?)", (.*?), cardStrings.DESCRIPTION, (.*?), (.*?), (.*?), (.*?)\);',SourceStr)
                d = re.findall("baseDamage = (.*?);",SourceStr)
                m = re.findall("baseMagicNumber = (.*?);",SourceStr)
                b = re.findall("baseBlock = (.*?);",SourceStr)
                # NL 回车      
                if s:
                  try:
                        description = cardJosn['DESCRIPTION']
                        if d!=[]:
                            description = description.replace("!D!",d)
                        if m!=[]:
                            description = description.replace("!M!",m)
                        if b !=[]:
                            description = description.replace("!B!",b)
                        description = description.replace("NL","")
                  # print(d,m,b)
                        tempData = dict(
                        name = cardJosn["NAME"],
                        # description = description,
                        cardId = cardId,
                        pngName = s.replace("/","_"),
                        cost = s.split(".")[-1],
                        cardType = s.split(".")[-1],
                        cardColor = s.split(".")[-1],
                        cardRarity = s.split(".")[-1],
                        )
                        if "DEPRECATED" not in tempData["name"]:
                            result.append(tempData)
                  except Exception as e:
                        pass
                        # print(e)
      os.chdir("../..")

    return result

if __name__ == '__main__':
    print(info())
```



这样最终生成的json包含卡牌的所有信息了,可以通过这些信息对卡牌资源进行拼接合并了,将其拼成一个完整具有文字说明的卡牌,这一步比较复杂,需要确定每个图片的位置所以这里我经过细致的条件后才能保证卡牌的样子和原来的差不多,这里我用到了opencv的createTrackbar, 可以实时预览图片位置,这样方便我进行调节,另外需要对不同类型的卡牌调用不同的卡牌UI,另外opencv处理图片不能在其上画中文,所以无奈我只能先转换换为PIL的Image对象,然后输入中文,将卡牌的描述输入上去,另外我需要控制每行输入的文本最大长度不超过卡牌本身的最大宽度,需要适时的加入换行,具体绘制卡牌的代码在 `drawCard.py` 中。

最终生成的卡牌:

!(https://img-blog.csdnimg.cn/20211103022127514.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Nob3V6aG91OTcwMQ==,size_16,color_FFFFFF,t_70)

这些卡牌图片资源结合我之前生成的具有卡牌详细信息json文件就可以写一个好用的存档修改器了。另外我还生成了遗物的json,不过遗物不需要拼接图片,只不过需要对文字说明进行一些处理。

或许直接在游戏中截图中比较方便,但是身为程序员我是不想做重复的劳动的。所有这些操作都是为了写一个好用的存档修改器。

### 4.种子信息的编码解码

在软件编写的过程中我也在反复查看存档中有用的数据,发现种子信息,种子信息采用的是java中长整形存储的,也就是 metric_seed_played,但是在游戏中我们看到的种子是字母形式的,不是数字形式的,通过分析我找到了对应转换的java代码:

```java
public static long getLong(String seedStr) {
    long total = 0L;
    seedStr = seedStr.toUpperCase().replaceAll("O", "0");
    for (int i = 0; i < seedStr.length(); i++) {
      char toFind = seedStr.charAt(i);
      int remainder = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".indexOf(toFind);
      if (remainder == -1)
      System.out.println("Character in seed is invalid: " + toFind);
      total *= "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".length();
      total += remainder;
    }
    return total;
}
public static String getString(long seed) {
    StringBuilder bldr = new StringBuilder();
    BigInteger leftover = new BigInteger(Long.toUnsignedString(seed));
    BigInteger charCount = BigInteger.valueOf("0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".length());
    while (!leftover.equals(BigInteger.ZERO)) {
      BigInteger remainder = leftover.remainder(charCount);
      leftover = leftover.divide(charCount);
      int charIndex = remainder.intValue();
      char c = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".charAt(charIndex);
      bldr.insert(0, c);
    }
    return bldr.toString();
}
```

简单写一个java 测试代码看看是否能正确进行转换:

```java
import java.io.*;
import java.util.*;
import java.math.BigInteger;

public class test {
public static long getLong(String seedStr) {
    long total = 0L;
    seedStr = seedStr.toUpperCase().replaceAll("O", "0");
    for (int i = 0; i < seedStr.length(); i++) {
      char toFind = seedStr.charAt(i);
      int remainder = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".indexOf(toFind);
      if (remainder == -1)
      System.out.println("Character in seed is invalid: " + toFind);
      total *= "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".length();
      total += remainder;
    }
    return total;
}


public static String getString(long seed) {
    StringBuilder bldr = new StringBuilder();
    BigInteger leftover = new BigInteger(Long.toUnsignedString(seed));
    BigInteger charCount = BigInteger.valueOf("0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".length());
    while (!leftover.equals(BigInteger.ZERO)) {
      BigInteger remainder = leftover.remainder(charCount);
      leftover = leftover.divide(charCount);
      int charIndex = remainder.intValue();
      char c = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".charAt(charIndex);
      bldr.insert(0, c);
    }
    return bldr.toString();
}
    public static void main(String[] args) {
      long seed = 6908306042847217830L;
      System.out.println(getString(seed));
    }
}

```



!(https://img-blog.csdnimg.cn/20211103024733350.png)

getString是将种子数字类型转换成字符串,getLong则相反。

python仿写:

```python
def getString(seed):
    bldr = ""
    char = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ"
    charCount = len(char)
    leftover = seed
    while leftover != 0:
      remainder = leftover % charCount
      leftover = leftover//charCount
      c = char
      bldr = c + bldr;
    return bldr

def getLong(seedStr):
    total = 0
    char = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ"
    seedStr = seedStr.upper().replace("O", "0")
    for i in range(len(seedStr)):
      toFind = seedStr
      remainder = char.index(toFind)
      total *= len(char)
      total += remainder
    return total

print(getString(6908306042847217830))

```

c++ 仿写:

```
QString DecodeGameSave::seedGetString(long long seed)
{
    QString bldr = "";
    QString char_ = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
    int charCount = char_.size();
    long long leftover = seed;
    int remainder;
    while (leftover != 0){
      remainder = leftover % charCount;
      leftover = leftover / charCount;
      bldr = char_ + bldr;
    }
    return bldr;
}
long long DecodeGameSave::seedGetLong(QString seedStr)
{
    long long total = 0;
    QString char_ = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
    seedStr = seedStr.toUpper().replace('O','0');
    for (int i=0;i<seedStr.size();i++){
      QChar toFind = seedStr;
      long long remainder = char_.indexOf(toFind);
      total *= char_.size();
      total += remainder;
    }
    return total;
}
```


所以这部分我也写到了软件中,可以显示种子字符串信息(没啥用其实)。当然种子只能将你此局的随机化确定,不能实现你修改完存档后种子分享给别人,然后别人也具有相同修改后的效果,只能保证随机化确定。

### 5.存档修改器编写

这里我用C++Qt5 框架进行编写的,c++用的是mingw,首先就是要用C++仿写存档加密解密算法:

```
QByteArray DecodeGameSave::encodeSave(const string &content)
{
    QByteArray byteArray = QByteArray::fromStdString(content);
    QByteArray key = "key";
    QByteArray newByteArry = {};
    for (int i=0;i<byteArray.size();i++) {
      newByteArry += byteArray^key;
    }
    return newByteArry.toBase64();
}

string DecodeGameSave::decodeSave(const QByteArray& content)
{
    QByteArray byteArray = QByteArray::fromBase64(content);
    QByteArray newByteArry = {};
    QByteArray key = "key";

    for (int i=0;i<byteArray.size();i++) {
      newByteArry += byteArray^key;
    }
    return newByteArry.toStdString();
}

```

然后就是需要解析我生成具有卡牌详细信息的 json 文件,因为需要cardId信息,还有一些分类信息。Qt自带 json解析的库,但是我怎么用都是不好使,后来无奈编译了一个jsoncpp这个库,我第一次知道编译工具竟然还有python的,叫scons, 无奈库中用的一些东西还都是python2的东西,所以我解决了一些报错,终于成功编译了。简单来说jsoncpp这个库中很多Json的结构都是 json::value对象,可以使用 asString来转换标准的c++ String对象,然后我用QString::fromStdString()将其转换成 QString对象。算是成功打通了数据的传递方面的问题了。

软件部分设计配置卡组功能是产生模态对话框,然后对话框中两个QlistWidget可以实现拖拽,显示卡牌用的是ListWidget控件,我刚开始想使用model-view架构,但是我发现listView的拖拽功能可能需要自己实现(我不会),算了吧,直接用listWidget也是个不错的选择。就是图片资源可能比较多,软件启动比较慢。另外卡牌缩放监听键鼠,Ctrl+鼠标滚轮,这里有意思的地方是滚动条会拦截到我的鼠标滚轮事件,所以我采用一个比较傻的解决办法,在按下Ctrl键的时候禁用QlistWidget这样就不会滚动条就不会拦截到我的鼠标滚轮事件了。

另外,我少判断了很多东西,例如人物只能使用对应的卡牌,而我没做判断,就会导致一个人物可以使用任意卡牌,游戏是不会报错的,另外还有 充能槽限制,任何人物都可以使用能量球:

!(https://img-blog.csdnimg.cn/20211109192844476.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Nob3V6aG91OTcwMQ==,size_16,color_FFFFFF,t_70)

并且卡牌都可以打出,所具有的效果也都会存在,所以我不加限制不但不是bug,反而是特色。

唯一的一个问题就是可能软件启动比较慢,因为加载的图片比较多(卡牌)。
另外我发现吾爱上竟然不能识别c++代码这种标签,佛了。

subney 发表于 2021-11-10 14:34

大龄爬塔勇士,已经爬不动了

yyah516 发表于 2021-11-10 09:17

以前这样的帖子标题为:分享一个修改器{:301_1004:}

流泪的小白 发表于 2021-11-10 10:21

这就很强了呀

6967632632 发表于 2021-11-9 20:52

这个是卡牌游戏看起很好玩~

dragonjelly 发表于 2021-11-9 22:53

感谢大佬分享思路及源码,收藏学习一波{:301_993:}

无秽之鸦 发表于 2021-11-10 00:50

感谢楼主分享虽然看不懂但感觉很厉害的样子~

tzlqjyx 发表于 2021-11-10 07:06

这游戏其实自己玩玩挺有意思的,哈哈,学习思路,但是不建议用

2212144372 发表于 2021-11-10 08:51

谢谢分享

越南邻国宰相 发表于 2021-11-10 09:35

收藏了666
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 写一个杀戮尖塔存档修改器