StarRailCopilot/doc/development.md
2020-06-13 19:26:09 +08:00

452 lines
14 KiB
Markdown

# 参与开发 Development
- 如何添加一个按钮 How to add a button
- 如何适配一张新的地图 How to adapt to a new map
- 如何支持其他服务器/语言 How to support other server/language
## 如何添加一个按钮 How to add a button
按钮文件按模块保存于 `./asset` 目录下, 按钮定义于每个模块的 `asset.py` 文件中.
比如 `BATTLE_PREPARATION` 按钮文件是这样的
![BATTLE_PREPARATION](development.assets/BATTLE_PREPARATION.png)
`asset.py` 它是这样的
```
BATTLE_PREPARATION = Button(area=(1043, 607, 1241, 667), color=(234, 179, 97), button=(1043, 607, 1241, 667), file='./assets/combat/BATTLE_PREPARATION.png')
```
注意, 所有的 `asset.py` 都是由 `./dev_tools/button_extract.py` 生成的, 不要手动去修改. 手动填写按钮的四角坐标是一件非常痛苦的事, 特别是当有大量的按钮或者按钮需要频繁修改的时候.
假设我们希望添加一个 `确定` 按钮, 它出现于潜艇信号扫描时.
1. **截图**
![screenshot](development.assets/screenshot.png)
2. **将图片复制到./asset下相应的目录中**, 更改文件名, 比如 `SEARCH_CONFIRM.png`
3. **拖动至Photoshop中**, 这里以 Photoshop CS6 为例
4. **使用选区工具框选按钮区域**
5. **播放动作**
第一次操作时, 可以按照以下步骤添加动作.
在菜单栏的 `窗口` 中, 点击 `动作` , 弹出动作窗口
在添加动作之前, 最好备份当前图片, 因为接下来需要记录的操作是不可逆的.
- 在动作窗口中, 点击新建动作的图标, 按照自己的喜好命名, 比如 `button_image`. 点击 `记录` , 注意灰色的圆圈变红了, 这表示动作录制开始了
- 在图片区域单击鼠标右键, 点击 `选择反向`
- 在菜单栏的 `编辑` 中, 点击 `填充`
- 在弹出的填充选项窗口中, 填充内容使用 `黑色` , 填充模式选择 `正常`, 不透明度选择 `100`, 点击 `确定`
- 在菜单栏的 `文件` 中, 点击 `保存`
- 在菜单栏的 `文件` 中, 点击 `关闭`
- 在动作窗口中, 单击停止录制的图标, 此时动作录制停止
录制完成后, 会得到动作如下
```
button_image
反向
填充
使用:黑色
不透明度:100%
模式:正常
储存
关闭
```
在以后的添加按钮的时候, 就可以直接点击 播放动作 的按钮, 完成图片处理
6. **(可选) 添加属性覆盖图片**
一个按钮具有三个属性:
- area, 按钮识别的区域
- color, 按钮的颜色
- button, 按钮出现后的点击区域
假如添在同一目录下放置图片文件 `SEARCH_CONFIRM.BUTTON.png` , 并按照刚才描述的方法处理图片. 那么这张图片的 `button` 属性将覆盖 `SEARCH_CONFIRM.png``button` 属性.
这是一个非常有用的特性, 因为脚本通常需要判断截图中出现的元素, 然后点击按钮, 需要判断的地方和需要点击的地方可能不出于同一位置.
7. **运行./dev_tools下的button_extract.py**
button_extract.py会自动提取按钮的属性, 免去了人工输入的烦恼
```
python -m dev_tools.button_extract
```
8. **使用按钮**
继承 module.base.base 下的 ModuleBase 类, 可以调用以下方法:
- appear()
- appear_then_click()
- wait_until_appear()
- wait_until_appear_then_click()
- wait_until_disappear()
方法的可选参数:
- offset (bool, int, tuple): 默认为0, 为0时将使用按钮在截图上的平均颜色识别按钮
输入后, 表示按钮参照预设的区域偏移的范围, 此时使用模板匹配识别按钮
- interval (int): 按钮的触发间隔, 默认为0.
代表按钮的触发间隔, 当按钮点击后有动画时, 这个参数可以防止按钮被频繁地点击.
- screenshot (bool): 按钮出现后保存截图
- genre (str): 截图保存的子目录名称
## 如何制作用于敌人识别的模板图片 How to make a template image for enemy detection
首先, 我们不能直接裁切截图来制作模板图片, 因为地图中的物体是有透视的. 我们需要使用 `dev_tools/relative_crop.py` 来获取图片. `get_relative_image` 可以根据透视裁剪出相对位置的图片, 并放大到固定的大小.
下图展示了`self.get_relative_image((-1, -1, 1, 0), output_shape=(120, 60))`的裁切区域
![relative_crop](development.assets/relative_crop.png)
运行 `dev_tools/relative_crop.py` 后, 会得到大量的临时图片, 找到对应格子的图片, 在图片中裁切出需要的模板.
将模板图片放置于 `assets/<server>/template` 目录下, 文件名需以 `TEMPLATE_` 开头, 最后运行 button_extract.
## 如何适配一张新的地图 How to adapt to a new map
下面举例适配 7-2 的简单版, 完整逻辑在 campaign.campaign_main.campaign_7_2
### 定义地图
1. **新建**
`./campaign` 下新建目录, 如果是新的活动图, 建议按照 `event_<活动时间>_<服务器>` 命名目录, 比如 event_20200326_cn.
新建 .py 文件, 文件名称为地图名, 小写, 以字母开头, 比如sp3, d3.
2. **导入**
```
from module.campaign.campaign_base import CampaignBase
from module.map.map_base import CampaignMap
```
3. ```
MAP = CampaignMap()
```
4. **(可选) 设置地图大小**
不填时, 根据海域信息生成
```
MAP.shape = 'H5'
```
5. **设置海域信息**
`碧蓝航线WIKI` 中查看敌人刷新信息, 如果是新地图, WIKI可能需要一两天的时间更新.
![wiki_7_2](development.assets/wiki_7_2.jpg)
```
MAP.map_data = '''
ME ++ ME -- ME ME -- SP
MM ++ ++ MM -- -- ME --
ME -- ME MB ME -- ME MM
-- ME -- MM -- ME ++ ++
SP -- ME ME -- ME ++ ++
'''
```
地图信息最少要包含海陆位置, 识别海陆现在还有一些困难
如果出生点比较偏僻的话, 还应包括出生点
```
MAP.map_data = '''
-- ++ ++ -- -- -- -- SP
-- ++ -- -- -- -- -- --
-- -- -- -- -- -- -- --
-- -- -- -- -- -- ++ ++
SP -- -- -- -- -- ++ ++
'''
```
关于这些符号的含义, 在 module.map.grid_info.py 中.
| print_name | property_name | description |
|------------|----------------|-------------------------|
| ++ | is_land | fleet can't go to land |
| -- | is_sea | sea |
| __ | | submarine spawn point |
| SP | is_spawn_point | fleet may spawns here |
| ME | may_enemy | enemy may spawns here |
| MB | may_boss | boss may spawns here |
| MM | may_mystery | mystery may spawns here |
| MA | may_ammo | fleet can get ammo here |
| MS | may_siren | Siren/Elite enemy spawn |
6. **(可选) 设置海域权重**
越高的数值表示越不希望舰队前往. 不设置时, 全部为10.
```
MAP.weight_data = '''
40 30 30 30 30 30 30 30
20 20 20 20 20 20 20 20
10 10 10 10 10 10 10 10
20 20 20 20 20 20 20 20
30 30 30 30 30 30 30 30
'''
```
7. **(可选) 设置相机位置**
```
MAP.camera_data = ['D3']
```
不设置时, 会根据地图大小和相机视野生成.
手动设置可以加快地图扫描的速度, 比如在 7-2 中, 自动生成的相机位置是 D2 D3 E2 E3, 实际上只需要将相机对准D3就可以看到整个地图(A1就不管了).
8. **(可选) 设置敌人刷新信息**
```
MAP.spawn_data = [
{'battle': 0, 'enemy': 3},
{'battle': 1, 'enemy': 2, 'mystery': 1},
{'battle': 2, 'enemy': 2, 'mystery': 1},
{'battle': 3, 'enemy': 1, 'mystery': 2},
{'battle': 4, 'enemy': 1},
{'battle': 5, 'boss': 1},
]
```
这里记录了每一战过后会刷新一些什么, 这个信息在 WIKI 上没有, 需要手动收集.
理论上, 不填写也可以运行. 敌人刷新信息的作用是纠正识别错误和缺失, 还有就是捕捉BOSS刷新时的相机移动
9. **展开地图**
```
A1, B1, C1, D1, E1, F1, G1, H1, \
A2, B2, C2, D2, E2, F2, G2, H2, \
A3, B3, C3, D3, E3, F3, G3, H3, \
A4, B4, C4, D4, E4, F4, G4, H4, \
A5, B5, C5, D5, E5, F5, G5, H5, \
= MAP.flatten()
```
这是为后续编写索敌逻辑做的准备, 可以使用以下代码生成
```
shape = 'H5'
def location2node(location):
return chr(location[0] + 64 + 1) + str(location[1] + 1)
def node2location(node):
return ord(node[0]) % 32 - 1, int(node[1]) - 1
shape = node2location(shape.upper())
for y in range(shape[1]+1):
text = ', '.join([location2node((x, y)) for x in range(shape[0]+1)]) + ', \\'
print(text)
print(' = MAP.flatten()')
```
10. **(可选) 定义地图元素**
比如定义路障
```
ROAD_MAIN = RoadGrids([A3, [C3, B4, C5], [F1, G2, G3]])
```
### 设置地图参数
地图参数将覆盖默认参数和用户参数, 具有最高优先.
如何不知道如何设置, 跳过即可. 但需要有定义.
```
class Config:
pass
```
可以设置的属性参照 module.config.config
比如, 对于微层混合D3, 因为地图加入了海雾, 需要不同的网格识别参数(默认参数是针对7-2的).
```
class Config:
INTERNAL_LINES_FIND_PEAKS_PARAMETERS = {
'height': (100, 220),
'width': 1,
'prominence': 10,
'distance': 35,
}
EDGE_LINES_FIND_PEAKS_PARAMETERS = {
'height': (255 - 80, 255),
'prominence': 2,
'distance': 50,
'wlen': 1000
}
```
对于活动图应该有以下设置, 开启精英识别.
```
class Config:
MAP_HAS_AMBUSH = False
MAP_HAS_FLEET_STEP = True
MAP_HAS_MOVABLE_ENEMY = True
MAP_HAS_SIREN = True
MAP_HAS_DYNAMIC_RED_BORDER = True
MAP_SIREN_COUNT = 2
```
如果前面仅输入了最少的海域信息, 或者没有敌人刷新信息, 应注明地图信息缺失, 此时会以开荒模式运行
```
class Config:
POOR_MAP_DATA = True
```
对于国服复刻苍红的回响, 抬高了相机位置, 网格缩小到0.66倍, 应该有
```
class Config:
MAP_GRID_CENTER_TOLERANCE = 0.3
INTERNAL_LINES_HOUGHLINES_THRESHOLD = 50
EDGE_LINES_HOUGHLINES_THRESHOLD = 50
CAMERA_SWIPE_MULTIPLY_X = 200 * 0.7
CAMERA_SWIPE_MULTIPLY_Y = 140 * 0.7
COINCIDENT_POINT_ENCOURAGE_DISTANCE = 1.
MID_DIFF_RANGE_H = (45, 70)
MID_DIFF_RANGE_V = (97 - 3, 97 + 3)
TRUST_EDGE_LINES = True
VANISH_POINT_RANGE = ((540, 740), (-4000, -2000))
DISTANCE_POINT_X_RANGE = ((-2000, -1000),)
INTERNAL_LINES_FIND_PEAKS_PARAMETERS = {
'height': (80, 255 - 40),
'width': (0.9, 10),
'prominence': 10,
'distance': 35,
'wlen': 100,
}
EDGE_LINES_FIND_PEAKS_PARAMETERS = {
'height': (255 - 40, 255),
'prominence': 10,
'distance': 50,
'wlen': 1000
}
```
### 编写索敌逻辑
- 可以不写, 全默认
```
class Campaign(CampaignBase):
MAP = MAP
```
- 简单的逻辑
按照第几战编写逻辑, 缺失时, 使用上一战的逻辑
一般而言, 每一战的逻辑应该以拣问号和清除路障开始, 再清除潜在的路障, 最后返回默认战斗逻辑
```
class Campaign(CampaignBase):
MAP = MAP
def battle_0(self):
self.clear_all_mystery(nearby=False)
if self.clear_roadblocks([ROAD_MAIN], strongest=True):
return True
if self.clear_potential_roadblocks([ROAD_MAIN], strongest=True):
return True
if self.clear_enemy(strongest=True, weight=True):
return True
return self.battle_default()
def battle_5(self):
self.clear_all_mystery(nearby=False)
if self.clear_roadblocks([ROAD_MAIN]):
return True
return self.fleet_2.brute_clear_boss()
```
未完待续
## 如何支持其他服务器/语言 How to support other server/language
### GUI
Copy `./module/config/argparser.py` to `argparser_xx.py` and change the argment.
Create a dictionary in `./module/config/dictionary.py` that translate your language to english.
Copy `alas_cn.py` to `alas_xx.py` and import `argparser_xx.py` . Then, edit server name.
> Format of .pyw file name: <sctipt_name>_<server_name>.pyw
>
> Script name is used to load ini file under `./config`, For example, alas_cn.pyw and alas_en.pyw both loads `./config/alas.ini`, but in different languages.
### Assets
Copy folder `./assets/cn` to `./assets/<your server>`, and replace the image. This will cost a lot of time to find, crop and test. Fortunately, if a image does not contain any charactors, it may works in all servers.
After replacing an image, don't forget to run `./dev_tools/button_extract.py`
### Class methods
Some method may be different in different servers. This decoractor is use to calls different function with a same name according to config (AzurLaneConfig instance).
```
from module.base.decorator import Config
from module.base.base import ModuleBase
class AnotherModule(ModuleBase):
@Config.when(SERVER='en')
def function(self):
# This method will be called only in EN server
pass
@Config.when(SERVER=None)
def function(self):
# This method will be called in other server
pass
```
### Other
There area also some modules difficult to change: the commission module.
In `./module/reward/commission.py`, I use [cnocr](https://github.com/breezedeus/cnocr) to recognize commission name in chinese, it may not works well in other languages.