Merge branch 'main' into beta

This commit is contained in:
David Capello 2024-06-10 18:58:26 -03:00
commit 801ea7ab6c
22 changed files with 264 additions and 92 deletions

View File

@ -8,35 +8,50 @@ jobs:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
build_type: [RelWithDebInfo, Debug]
enable_ui: [off]
include:
- os: ubuntu-latest
build_type: Debug
enable_ui: on
ui: [gui, cli]
scripting: [lua, noscripts]
exclude:
- build_type: Debug
ui: gui
- build_type: RelWithDebInfo
ui: cli
- build_type: RelWithDebInfo
scripting: noscripts
steps:
- uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Install Dependencies
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y \
libpixman-1-dev libfreetype6-dev libharfbuzz-dev zlib1g-dev \
libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev
- name: Install Skia
if: ${{ matrix.ui == 'gui' }}
shell: bash
run: |
if [[ "${{ runner.os }}" == "Windows" ]] ; then
choco install wget -y --no-progress
wget https://github.com/aseprite/skia/releases/download/m124-08a5439a6b/Skia-Windows-Release-x64.zip
unzip Skia-Windows-Release-x64.zip -d skia
elif [[ "${{ runner.os }}" == "macOS" ]] ; then
wget https://github.com/aseprite/skia/releases/download/m124-08a5439a6b/Skia-macOS-Release-arm64.zip
unzip Skia-macOS-Release-arm64.zip -d skia
else
wget https://github.com/aseprite/skia/releases/download/m124-08a5439a6b/Skia-Linux-Release-x64.zip
unzip Skia-Linux-Release-x64.zip -d skia
fi
- name: ccache
uses: hendrikmuhs/ccache-action@v1
if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }}
with:
key: ${{ matrix.os }}-${{ matrix.enable_ui }}-${{ matrix.build_type }}
key: ${{ matrix.os }}-${{ matrix.ui }}-${{ matrix.scripting }}-${{ matrix.build_type }}
- uses: aseprite/get-ninja@main
- uses: ilammy/msvc-dev-cmd@v1
if: runner.os == 'Windows'
- name: Workaround for windows-2022 and cmake 3.25.0
if: runner.os == 'Windows'
shell: bash
run: rm -rf C:/Strawberry/
- name: Install Dependencies
shell: bash
run: |
if [[ "${{ runner.os }}" == "Linux" ]] ; then
sudo apt-get update -qq
sudo apt-get install -y \
libx11-dev libxcursor-dev libxi-dev
fi
- name: Generating Makefiles
shell: bash
run: |
@ -45,12 +60,37 @@ jobs:
else
export enable_ccache=on
fi
if [[ "${{ matrix.ui }}" == "gui" ]] ; then
export enable_ui=on
export laf_backend=skia
else
export enable_ui=off
export laf_backend=none
fi
if [[ "${{ matrix.scripting }}" == "lua" ]] ; then
export enable_scripting=on
else
export enable_scripting=off
fi
if [[ "${{ runner.os }}" == "macOS" ]] ; then
export skia_arch=arm64
else
export skia_arch=x64
fi
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.9 \
-DENABLE_TESTS=ON \
-DENABLE_UI=${{ matrix.enable_ui }} \
-DENABLE_CCACHE=$enable_ccache
-DENABLE_UI=$enable_ui \
-DENABLE_SCRIPTING=$enable_scripting \
-DENABLE_CCACHE=$enable_ccache \
-DLAF_BACKEND=$laf_backend \
-DSKIA_DIR=$(realpath skia) \
-DSKIA_LIBRARY_DIR=$(realpath skia/out/Release-$skia_arch)
- name: Compiling
shell: bash
run: |
@ -63,6 +103,7 @@ jobs:
fi
cd build && $XVFB ctest --output-on-failure
- name: Running CLI Tests
if: ${{ matrix.scripting == 'lua' }}
shell: bash
run: |
if [[ "${{ runner.os }}" == "Linux" ]] ; then

72
.github/workflows/lint_commit.yml vendored Normal file
View File

@ -0,0 +1,72 @@
# Based on SerenityOS commit linter:
# https://github.com/SerenityOS/serenity/blob/master/.github/workflows/lintcommits.yml
name: Commit linter
on: [pull_request_target]
jobs:
lint_commits:
runs-on: ubuntu-22.04
if: always() && github.repository == 'aseprite/aseprite'
steps:
- name: Lint PR commits
uses: actions/github-script@v7
with:
script: |
const rules = [
{
pattern: /^[^\r]*$/,
error: "Commit message contains CRLF line breaks (only unix-style LF linebreaks are allowed)",
},
{
pattern: /^.+(\r?\n(\r?\n.*)*)?$/,
error: "Empty line between commit title and body is missing",
},
{
pattern: /^.{0,72}(?:\r?\n(?:(.{0,72})|(.*?([a-z]+:\/\/)?(([a-zA-Z0-9_]|-)+\.)+[a-z]{2,}(:\d+)?([a-zA-Z_0-9@:%\+.~\?&/=]|-)+).*?))*$/,
error: "Commit message lines are too long (maximum allowed is 72 characters, except for URLs)",
},
{
pattern: /^.+[^.\n](\r?\n.*)*$/,
error: "Commit title ends in a period",
},
{
pattern: /^((?!Signed-off-by: )[\s\S])*$/,
error: "Commit body contains a Signed-off-by tag",
},
];
const { repository, pull_request } = context.payload;
// NOTE: This maxes out at 250 commits. If this becomes a problem, see:
// https://octokit.github.io/rest.js/v18#pulls-list-commits
const opts = github.rest.pulls.listCommits.endpoint.merge({
owner: repository.owner.login,
repo: repository.name,
pull_number: pull_request.number,
});
const commits = await github.paginate(opts);
const errors = [];
for (const { sha, commit: { message } } of commits) {
const commitErrors = [];
for (const { pattern, error } of rules) {
if (!pattern.test(message)) {
commitErrors.push(error);
}
}
if (commitErrors.length > 0) {
const title = message.split("\n")[0];
errors.push([`${title} (${sha}):`, ...commitErrors].join("\n "));
}
}
if (errors.length > 0) {
core.setFailed(`One or more of the commits in this PR do not match the code submission policy:\n\n${errors.join("\n")}`);
}
- name: Comment on PR
if: ${{ failure() && !github.event.pull_request.draft }}
uses: IdanHo/comment-on-pr@63ea2bf352997c66e524b8b5be7a79163fb3a88a
env:
GITHUB_TOKEN: ${{ secrets.LINT_COMMIT_TOKEN }}
with:
msg: "Hi there!\n\nOne or more of the commit messages in this PR do not match our [code submission policy](https://github.com/aseprite/aseprite/blob/main/CONTRIBUTING.md), please check the `lint_commits` CI job for more details on which commits were flagged and why.\nPlease do not close this PR and open another, instead modify your commit message(s) with [git commit --amend](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/changing-a-commit-message) and force push those changes to update this PR."

View File

@ -1,7 +1,7 @@
# Code of Conduct
We have a [code of conduct](CODE_OF_CONDUCT.md) that we all must
read. Be polite to everyone. If you are not in your best day, take a
follow. Be polite to everyone. If you are not in your best day, take a
deep breath and try again. Smile :smile:
# New Issues
@ -24,31 +24,33 @@ check the following items:
* See how to get the source code correctly in the [INSTALL](INSTALL.md) guide.
* Check if you are using the latest repository clone.
* Remember that we use submodules, so you need to initialize and update them.
* Search in the [GitHub issues about compilation](https://github.com/aseprite/aseprite/issues?q=is%3Aissue+label%3Acompilation)
if someone else had the same problem.
* Remember that might be some [pull requests](https://github.com/aseprite/aseprite/pulls)
being reviewed to fix your same problem.
being reviewed to fix your same issue.
If you have a compilation problem, you can ask in the
[Development category](https://community.aseprite.org/c/development)
in the [Community site](https://community.aseprite.org/) for help
or creating a [GitHub issue](https://github.com/aseprite/aseprite/issues/new).
or create a [new GitHub issue](https://github.com/aseprite/aseprite/issues/new).
# Contributing
One of the easiest ways to contribute is writing articles,
[Steam reviews](https://steamcommunity.com/app/431730/reviews/),
blog posts, recording video tutorials,
[creating pixel art](https://aseprite.deviantart.com/), or showing your love
to Aseprite e.g. naming Aseprite in your website and linking it to
https://www.aseprite.org/, following
[@aseprite](https://twitter.com/aseprite) twitter account, or
[buying an extra Aseprite copy to your friend](https://www.aseprite.org/download/).
One of the easiest ways to contribute is writing articles, [Steam
reviews](https://steamcommunity.com/app/431730/reviews/), blog posts,
recording video tutorials, creating pixel art in social media with
[#aseprite](https://twitter.com/search?q=%23aseprite), or showing your
love to Aseprite, e.g. naming Aseprite in your website and linking it
to https://www.aseprite.org/, following [@aseprite](https://twitter.com/aseprite),
or [buying an extra Aseprite copy to your friends](https://www.aseprite.org/download/).
Other ways to contribute require direct contact with us. For example:
* [Writing documentation](https://github.com/aseprite/docs).
* Making art with Aseprite and for Aseprite (logos, skins, mockups).
* Making art (and fan art) with Aseprite and for Aseprite (logos, skins, mockups).
* Sending patches for features or bug fixes.
* Reviewing issues in the [issue tracker](https://github.com/aseprite/aseprite/issues) and making comments.
* Reviewing issues in the [issue tracker](https://github.com/aseprite/aseprite/issues)
and making comments.
* Helping other users in the [Community](https://community.aseprite.org/) site.
## Documentation
@ -56,15 +58,16 @@ Other ways to contribute require direct contact with us. For example:
You can start seeing the
[documentation](https://www.aseprite.org/docs/), and
[contact us](mailto:support@aseprite.org) if you want to help
writting documentation
or recording [tutorials](https://www.aseprite.org/docs/tutorial/).
writing documentation or recording [tutorials](https://www.aseprite.org/docs/tutorial/).
If you are going to write documentation, we recommend you to take
screenshots or record a GIF animations to show steps:
If you are going to write some docs, we recommend you to take
screenshots or record a GIF animations to showcase the feature your
are documenting or the steps to follow:
* As screen recording software, on Windows you can generate GIF files
using [LICEcap](http://www.cockos.com/licecap/).
* You can upload the PNG/GIF images to [Imgur](http://imgur.com/).
* PNG/GIF images can be uploaded in the same docs repository
[with a pull request](https://github.com/aseprite/docs/pulls)
## Reviewing Issues
@ -74,54 +77,54 @@ new [features](https://community.aseprite.org/c/features),
[bug reports](https://community.aseprite.org/c/bugs), etc. You are
encouraged to create mockups for any issue you see and attach them.
## Hacking
## Code submission policy
The first thing to keep in mind if you want to modify the source code:
checkout the **main** branch. It is the branch that we use to
develop new features and fix issues that are planned for the next big
release. See the [INSTALL](INSTALL.md) guide to know how to compile.
We have some rules for the changes and commits that are contributed:
To start looking the source code, see how it is organized in
[src/README.md](https://github.com/aseprite/aseprite/tree/main/src/#aseprite-source-code)
file.
* First of all you will need to sign our
[Contributor License Agreement](https://github.com/igarastudio/cla) (CLA)
to submit your code.
* Split your changes in the most atomic commits possible: one commit
for feature, or fix.
* Rebase your commits to the `main` branch (or `beta` if you are
targeting the beta version).
* Wrap your commit messages at 72 characters.
* The first line of the commit message is the subject line.
* Write the subject line in the imperative mood, e.g. "Fix something",
not "Fixed something".
* For platform-specific commits start the subject line using
`[win]`, `[osx]`, or `[x11]` prefixes.
* For CLI related commits you can use the `[cli]` prefix in the
subject line.
* For Lua scripting related commits can use the `[lua]` prefix in
the subject line.
* Check the spelling of your code, comments and commit messages.
* Follow our [coding style guide](docs/CODING_STYLE.md). We're using
some C++17 features, targeting macOS 10.9 mainly as the oldest
platform (and the one limiting us to newer C++ standards),
## Forking & Pull Requests
You can also take a look at the [src/README.md](https://github.com/aseprite/aseprite/tree/main/src/#aseprite-source-code)
guide which contains some information about how the code is structured.
You can fork the GitHub repository using the Fork button at
[https://github.com/aseprite/aseprite](https://github.com/aseprite/aseprite).
The Pull Requests (PR) systems works in this way:
1. First of all you will need to sign our
[Contributor License Agreement](https://github.com/igarastudio/cla) (CLA).
1. Then you can start working on Aseprite. Create a new branch from `main`, e.g. `fix-8` to fix the issue 8.
Check this guide about [how to name your branch](https://github.com/agis/git-style-guide#branches).
1. Start working on that new branch, and push your commits to your fork.
1. Create a new PR to merge your `fix-8` branch to the official `main`.
1. If the PR is accepted (does not require review/comments/modifications),
your branch is merged into `main`.
1. You will need to pull changes from the official `main` branch, and
merge them in your own `main` branch. Finally you can discard your
own `fix-8` branch (because those changes should be already merged
into `main` if the PR was accepted).
1. Continue working from the new `main` head.
To keep in mind: **always** start working from the `main` head, if you
want to fix three different issues, create three different branches
from `main` and then send three different PR. Do not chain all the
fixes in one single branch. E.g. `fix-issues-3-and-8-and-25`.
## Community
# Community
You can use the [Development category](https://community.aseprite.org/c/development)
to ask question about the code, how to compile, etc.
If you want to start working in something
to ask questions about the code, how to compile, etc.
If you want to start working on something
([issue](https://github.com/aseprite/aseprite/issues),
[bug](https://community.aseprite.org/c/bugs),
or [feature](https://community.aseprite.org/c/features)),
post a comment asking if somebody is already working on that,
in that way you can avoid start programming in something that is already
in that way you can avoid starting programming in something that is already
done for the next release or which someone else is working on.
And always remember to take a look at our
[roadmap](http://www.aseprite.org/roadmap/).
# Future
If you want to contribute a new feature, I highly recommend trying to
contribute a couple of pull requests to fix some bugs first. After
that you can check what are the features we're planning for the
future:
* Our [roadmap](http://www.aseprite.org/roadmap/) and our [planning](https://github.com/orgs/aseprite/projects/10).
* The most liked [issues on GitHub](https://github.com/aseprite/aseprite/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc).
* [Features on the forum](https://community.aseprite.org/c/features/7/l/latest?order=votes) with the most votes.

2
laf

@ -1 +1 @@
Subproject commit c90b81aec001293bdf7d19eb7feb22509716705e
Subproject commit 10caee2455d07676d79a69a102399080f5eba4a9

View File

@ -74,6 +74,7 @@ AppOptions::AppOptions(int argc, const char* argv[])
, m_scriptParam(m_po.add("script-param").requiresValue("name=value").description("Parameter for a script executed from the\nCLI that you can access with app.params"))
#endif
, m_listLayers(m_po.add("list-layers").description("List layers of the next given sprite\nor include layers in JSON data"))
, m_listLayerHierarchy(m_po.add("list-layer-hierarchy").description("List layers with groups of the next given sprite\nor include layers hierarchy in JSON data"))
, m_listTags(m_po.add("list-tags").description("List tags of the next given sprite\nor include frame tags in JSON data"))
, m_listSlices(m_po.add("list-slices").description("List slices of the next given sprite\nor include slices in JSON data"))
, m_oneFrame(m_po.add("oneframe").description("Load just the first frame"))

View File

@ -90,6 +90,7 @@ public:
const Option& scriptParam() const { return m_scriptParam; }
#endif
const Option& listLayers() const { return m_listLayers; }
const Option& listLayerHierarchy() const { return m_listLayerHierarchy; }
const Option& listTags() const { return m_listTags; }
const Option& listSlices() const { return m_listSlices; }
const Option& oneFrame() const { return m_oneFrame; }
@ -164,6 +165,7 @@ private:
Option& m_scriptParam;
#endif
Option& m_listLayers;
Option& m_listLayerHierarchy;
Option& m_listTags;
Option& m_listSlices;
Option& m_oneFrame;

View File

@ -37,6 +37,7 @@ namespace app {
bool splitGrid = false;
bool allLayers = false;
bool listLayers = false;
bool listLayerHierarchy = false;
bool listTags = false;
bool listSlices = false;
bool ignoreEmpty = false;

View File

@ -587,6 +587,13 @@ int CliProcessor::process(Context* ctx)
else
cof.listLayers = true;
}
// --list-layer-hierarchy
else if (opt == &m_options.listLayerHierarchy()) {
if (m_exporter)
m_exporter->setListLayerHierarchy(true);
else
cof.listLayerHierarchy = true;
}
// --list-tags
else if (opt == &m_options.listTags()) {
if (m_exporter)

View File

@ -67,6 +67,10 @@ void DefaultCliDelegate::afterOpenFile(const CliOpenFile& cof)
std::cout << layer->name() << "\n";
}
if (cof.listLayerHierarchy) {
std::cout << cof.document->sprite()->visibleLayerHierarchyAsString() << "\n";
}
if (cof.listTags) {
for (doc::Tag* tag : cof.document->sprite()->tags())
std::cout << tag->name() << "\n";

View File

@ -67,6 +67,9 @@ void PreviewCliDelegate::afterOpenFile(const CliOpenFile& cof)
if (cof.listLayers)
std::cout << " - List layers\n";
if (cof.listLayerHierarchy)
std::cout << " - List layer hierarchy\n";
if (cof.listTags)
std::cout << " - List tags\n";

View File

@ -623,6 +623,7 @@ void DocExporter::reset()
m_splitTags = false;
m_listTags = false;
m_listLayers = false;
m_listLayerHierarchy = false;
m_listSlices = false;
m_documents.clear();
}
@ -1487,7 +1488,7 @@ void DocExporter::createDataFile(const Samples& samples,
}
// meta.layers
if (m_listLayers) {
if (m_listLayers || m_listLayerHierarchy) {
LayerList metaLayers;
for (auto& item : m_documents) {
if (item.isOneImageOnly())

View File

@ -77,6 +77,7 @@ namespace app {
void setSplitTags(bool splitTags) { m_splitTags = splitTags; }
void setListTags(bool value) { m_listTags = value; }
void setListLayers(bool value) { m_listLayers = value; }
void setListLayerHierarchy(bool value) { m_listLayerHierarchy = value; }
void setListSlices(bool value) { m_listSlices = value; }
void addImage(
@ -180,6 +181,7 @@ namespace app {
bool m_splitTags;
bool m_listTags;
bool m_listLayers;
bool m_listLayerHierarchy;
bool m_listSlices;
Items m_documents;

View File

@ -50,6 +50,13 @@ FILE* lua_user_fopen(const char* fname,
return base::open_file_raw(fname, mode);
}
FILE* lua_user_freopen(const char* fname,
const char* mode,
FILE* stream)
{
return base::reopen_file_raw(fname, mode, stream);
}
namespace app {
namespace script {

View File

@ -540,6 +540,20 @@ void LayerGroup::allTilemaps(LayerList& list) const
}
}
std::string LayerGroup::visibleLayerHierarchyAsString(const std::string& indent) const
{
std::string str;
for (Layer* child : m_layers) {
if (!child->isVisible())
continue;
str += indent + child->name() + (child->isGroup() ? "/" : "") + "\n";
if (child->isGroup())
str += static_cast<LayerGroup*>(child)->visibleLayerHierarchyAsString(indent+" ");
}
return str;
}
void LayerGroup::getCels(CelList& cels) const
{
for (const Layer* layer : m_layers)

View File

@ -215,6 +215,7 @@ namespace doc {
void allVisibleReferenceLayers(LayerList& list) const;
void allBrowsableLayers(LayerList& list) const;
void allTilemaps(LayerList& list) const;
std::string visibleLayerHierarchyAsString(const std::string& indent) const;
void getCels(CelList& cels) const override;
void displaceFrames(frame_t fromThis, frame_t delta) override;

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020-2022 Igara Studio S.A.
// Copyright (c) 2020-2024 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -11,6 +11,7 @@
#include "base/debug.h"
#include "base/disable_copying.h"
#include "base/ints.h"
#include "doc/object.h"
#include "doc/rgbmap.h"
@ -23,7 +24,7 @@ namespace doc {
// It acts like a cache for Palette:findBestfit() calls.
class RgbMapRGB5A3 : public RgbMap {
// Bit activated on m_map entries that aren't yet calculated.
const int INVALID = 256;
const uint16_t INVALID = 256;
public:
RgbMapRGB5A3();
@ -31,13 +32,13 @@ namespace doc {
// RgbMap impl
void regenerateMap(const Palette* palette, int maskIndex) override;
int mapColor(const color_t rgba) const override {
const int r = rgba_getr(rgba);
const int g = rgba_getg(rgba);
const int b = rgba_getb(rgba);
const int a = rgba_geta(rgba);
const uint8_t r = rgba_getr(rgba);
const uint8_t g = rgba_getg(rgba);
const uint8_t b = rgba_getb(rgba);
const uint8_t a = rgba_geta(rgba);
// bits -> bbbbbgggggrrrrraaa
const int i = (a>>5) | ((b>>3) << 3) | ((g>>3) << 8) | ((r>>3) << 13);
const int v = m_map[i];
const uint32_t i = (a>>5) | ((b>>3) << 3) | ((g>>3) << 8) | ((r>>3) << 13);
const uint16_t v = m_map[i];
return (v & INVALID) ? generateEntry(i, r, g, b, a): v;
}

View File

@ -773,6 +773,11 @@ LayerList Sprite::allTilemaps() const
return list;
}
std::string Sprite::visibleLayerHierarchyAsString() const
{
return m_root->visibleLayerHierarchyAsString("");
}
CelsRange Sprite::cels() const
{
SelectedFrames selFrames;

View File

@ -219,6 +219,7 @@ namespace doc {
LayerList allVisibleReferenceLayers() const;
LayerList allBrowsableLayers() const;
LayerList allTilemaps() const;
std::string visibleLayerHierarchyAsString() const;
CelsRange cels() const;
CelsRange cels(frame_t frame) const;

View File

@ -8,3 +8,9 @@ assert(100 == math.min(100, 200, 300))
assert(300 == math.max(100, 200, 300))
assert(50 == math.fmod(250, 100))
assert(3141 == math.floor(1000*math.pi))
-- Add tests for integer <-> number comparisons
assert(math.floor(0.5) == 0)
assert(math.floor(0.5) == 0.0)
assert(math.floor(0.5) ~= 0.5)
assert(math.floor(0.5)+0.1-0.1 ~= 0.5)

View File

@ -133,8 +133,8 @@ for _,cm in ipairs{ ColorMode.RGB,
app.sprite = spr
app.command.SaveFileCopyAs{ filename=fn, slice="small_slice", scale=scale }
local c = app.open(fn)
assert(c.width == slice.bounds.width*scale)
assert(c.height == slice.bounds.height*scale)
assert(c.width == math.floor(slice.bounds.width*scale))
assert(c.height == math.floor(slice.bounds.height*scale))
local testImg = Image(spr.cels[1].image, spr.slices[1].bounds)
fix_images(testImg, scale, fileExt, c, cm, c1)

View File

@ -21,8 +21,8 @@ do
a:resize(6, 8)
assert(a.width == 6)
assert(a.height == 8)
assert(a.cels[1].image.width == 32 * 6 / 4) -- Check that the image was resized (not only the canvas)
assert(a.cels[1].image.height == 64 * 8 / 5)
assert(a.cels[1].image.width == math.floor(32 * 6 / 4)) -- Check that the image was resized (not only the canvas)
assert(a.cels[1].image.height == math.floor(64 * 8 / 5))
a:crop{x=-1, y=-1, width=20, height=30}
assert(a.width == 20)
assert(a.height == 30)

2
third_party/lua vendored

@ -1 +1 @@
Subproject commit 04abf20c9e8c973c62b30d0b98f6884ff665b1ba
Subproject commit 2a00e6b0013f54ce80b6e3cefe6514e13229987a